功能:唱歌跳舞视频正常并且能够重新下载处理
This commit is contained in:
parent
57a846b2a1
commit
d7ec1d530a
|
|
@ -1,2 +1,2 @@
|
||||||
DATABASE_URL=mysql+pymysql://root:root@127.0.0.1:3306/lover?charset=utf8mb4
|
DATABASE_URL=mysql+pymysql://root:root@127.0.0.1:3306/lover?charset=utf8mb4
|
||||||
USER_INFO_API=http://192.168.1.164:30100/index.php/api/user_basic/get_user_basic
|
USER_INFO_API=http://192.168.1.164:30100/api/user_basic/get_user_basic
|
||||||
|
|
@ -150,7 +150,7 @@ class Settings(BaseSettings):
|
||||||
|
|
||||||
# 用户信息拉取接口(FastAdmin 提供)
|
# 用户信息拉取接口(FastAdmin 提供)
|
||||||
USER_INFO_API: str = Field(
|
USER_INFO_API: str = Field(
|
||||||
default="http://192.168.1.164:30100/index.php/api/user_basic/get_user_basic",
|
default="http://192.168.1.164:30100/api/user_basic/get_user_basic",
|
||||||
env="USER_INFO_API",
|
env="USER_INFO_API",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,15 @@ def _fetch_user_from_php(token: str) -> Optional[dict]:
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
logger.info(f"用户中心调试 - 调用接口: {settings.USER_INFO_API}")
|
# 临时硬编码修复配置问题
|
||||||
|
user_info_api = "http://192.168.1.164:30100/api/user_basic/get_user_basic"
|
||||||
|
|
||||||
|
logger.info(f"用户中心调试 - 调用接口: {user_info_api}")
|
||||||
logger.info(f"用户中心调试 - token: {token}")
|
logger.info(f"用户中心调试 - token: {token}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
settings.USER_INFO_API,
|
user_info_api,
|
||||||
headers={"token": token},
|
headers={"token": token},
|
||||||
timeout=5,
|
timeout=5,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,4 @@ requests>=2.31
|
||||||
oss2>=2.18
|
oss2>=2.18
|
||||||
dashscope>=1.20
|
dashscope>=1.20
|
||||||
pyyaml>=6.0
|
pyyaml>=6.0
|
||||||
|
imageio-ffmpeg>=0.4
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
@ -27,8 +28,43 @@ from ..models import (
|
||||||
)
|
)
|
||||||
from ..response import ApiResponse, success_response
|
from ..response import ApiResponse, success_response
|
||||||
|
|
||||||
|
try:
|
||||||
|
import imageio_ffmpeg # type: ignore
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
imageio_ffmpeg = None
|
||||||
|
|
||||||
router = APIRouter(prefix="/dance", tags=["dance"])
|
router = APIRouter(prefix="/dance", tags=["dance"])
|
||||||
|
|
||||||
|
|
||||||
|
def _ffmpeg_bin() -> str:
|
||||||
|
found = shutil.which("ffmpeg")
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
if imageio_ffmpeg is not None:
|
||||||
|
try:
|
||||||
|
return imageio_ffmpeg.get_ffmpeg_exe()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "ffmpeg"
|
||||||
|
|
||||||
|
|
||||||
|
def _ffprobe_bin() -> str:
|
||||||
|
found = shutil.which("ffprobe")
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
if imageio_ffmpeg is not None:
|
||||||
|
try:
|
||||||
|
ffmpeg_path = imageio_ffmpeg.get_ffmpeg_exe()
|
||||||
|
candidate = os.path.join(os.path.dirname(ffmpeg_path), "ffprobe.exe")
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
return candidate
|
||||||
|
candidate = os.path.join(os.path.dirname(ffmpeg_path), "ffprobe")
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
return candidate
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "ffprobe"
|
||||||
|
|
||||||
DANCE_TARGET_DURATION_SEC = 10
|
DANCE_TARGET_DURATION_SEC = 10
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -53,6 +89,7 @@ def get_dance_history(
|
||||||
GenerationTask.user_id == user.id,
|
GenerationTask.user_id == user.id,
|
||||||
GenerationTask.lover_id == lover.id,
|
GenerationTask.lover_id == lover.id,
|
||||||
GenerationTask.task_type == "video",
|
GenerationTask.task_type == "video",
|
||||||
|
GenerationTask.payload["prompt"].as_string().isnot(None),
|
||||||
GenerationTask.status == "succeeded",
|
GenerationTask.status == "succeeded",
|
||||||
GenerationTask.result_url.isnot(None),
|
GenerationTask.result_url.isnot(None),
|
||||||
)
|
)
|
||||||
|
|
@ -77,6 +114,154 @@ def get_dance_history(
|
||||||
return success_response(result, msg="获取成功")
|
return success_response(result, msg="获取成功")
|
||||||
|
|
||||||
|
|
||||||
|
def _download_binary(url: str) -> bytes:
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, timeout=30)
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=502, detail="文件下载失败") from exc
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise HTTPException(status_code=502, detail="文件下载失败")
|
||||||
|
return resp.content
|
||||||
|
|
||||||
|
|
||||||
|
def _retry_finalize_dance_task(task_id: int) -> None:
|
||||||
|
"""任务失败但 DashScope 端已成功时,尝试重新下载视频并重新上传(自愈/手动重试共用)。"""
|
||||||
|
try:
|
||||||
|
with SessionLocal() as db:
|
||||||
|
task = (
|
||||||
|
db.query(GenerationTask)
|
||||||
|
.filter(GenerationTask.id == task_id)
|
||||||
|
.with_for_update()
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not task:
|
||||||
|
return
|
||||||
|
payload = task.payload or {}
|
||||||
|
dash_id = payload.get("dashscope_task_id")
|
||||||
|
if not dash_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
status, dash_video_url = _fetch_dashscope_status(str(dash_id))
|
||||||
|
if status != "SUCCEEDED" or not dash_video_url:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 重新生成:下载 dashscope 视频 -> 随机 BGM -> 合成 -> 上传
|
||||||
|
bgm_song = _pick_random_bgm(db)
|
||||||
|
bgm_audio_url_raw = bgm_song.audio_url
|
||||||
|
bgm_audio_url = _cdnize(bgm_audio_url_raw) or bgm_audio_url_raw
|
||||||
|
merged_bytes, bgm_meta = _merge_dance_video_with_bgm(
|
||||||
|
dash_video_url,
|
||||||
|
bgm_audio_url,
|
||||||
|
DANCE_TARGET_DURATION_SEC,
|
||||||
|
)
|
||||||
|
object_name = f"lover/{task.lover_id}/dance/{int(time.time())}_retry.mp4"
|
||||||
|
oss_url = _upload_to_oss(merged_bytes, object_name)
|
||||||
|
|
||||||
|
task.status = "succeeded"
|
||||||
|
task.result_url = oss_url
|
||||||
|
task.error_msg = None
|
||||||
|
task.payload = {
|
||||||
|
**payload,
|
||||||
|
"dashscope_video_url": dash_video_url,
|
||||||
|
"bgm_song_id": bgm_song.id,
|
||||||
|
"bgm_audio_url": bgm_audio_url,
|
||||||
|
"bgm_audio_url_raw": bgm_audio_url_raw,
|
||||||
|
"bgm_start_sec": bgm_meta.get("bgm_start_sec"),
|
||||||
|
"bgm_duration": DANCE_TARGET_DURATION_SEC,
|
||||||
|
"retry_finalized": True,
|
||||||
|
}
|
||||||
|
task.updated_at = datetime.utcnow()
|
||||||
|
db.add(task)
|
||||||
|
db.commit()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/retry/{task_id}", response_model=ApiResponse[dict])
|
||||||
|
def retry_dance_task(
|
||||||
|
task_id: int,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: AuthedUser = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""手动重试:用于 DashScope 端已成功但本地下载/合成/上传失败导致任务失败的情况。"""
|
||||||
|
lover = db.query(Lover).filter(Lover.user_id == user.id).first()
|
||||||
|
if not lover:
|
||||||
|
raise HTTPException(status_code=404, detail="恋人未找到")
|
||||||
|
|
||||||
|
task = (
|
||||||
|
db.query(GenerationTask)
|
||||||
|
.filter(
|
||||||
|
GenerationTask.id == task_id,
|
||||||
|
GenerationTask.user_id == user.id,
|
||||||
|
GenerationTask.lover_id == lover.id,
|
||||||
|
GenerationTask.task_type == "video",
|
||||||
|
GenerationTask.payload["prompt"].as_string().isnot(None),
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="任务不存在")
|
||||||
|
|
||||||
|
payload = task.payload or {}
|
||||||
|
dash_id = payload.get("dashscope_task_id")
|
||||||
|
if not dash_id:
|
||||||
|
raise HTTPException(status_code=400, detail="任务缺少 dashscope_task_id,无法重试")
|
||||||
|
|
||||||
|
task.payload = {**payload, "manual_retry": True}
|
||||||
|
task.updated_at = datetime.utcnow()
|
||||||
|
db.add(task)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
background_tasks.add_task(_retry_finalize_dance_task, int(task.id))
|
||||||
|
return success_response({"task_id": int(task.id)}, msg="已触发重试")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/history/all", response_model=ApiResponse[list[dict]])
|
||||||
|
def get_dance_history_all(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: AuthedUser = Depends(get_current_user),
|
||||||
|
page: int = 1,
|
||||||
|
size: int = 20,
|
||||||
|
):
|
||||||
|
"""获取用户的跳舞视频全部历史记录(成功+失败+进行中)。"""
|
||||||
|
lover = db.query(Lover).filter(Lover.user_id == user.id).first()
|
||||||
|
if not lover:
|
||||||
|
raise HTTPException(status_code=404, detail="恋人未找到")
|
||||||
|
|
||||||
|
offset = (page - 1) * size
|
||||||
|
tasks = (
|
||||||
|
db.query(GenerationTask)
|
||||||
|
.filter(
|
||||||
|
GenerationTask.user_id == user.id,
|
||||||
|
GenerationTask.lover_id == lover.id,
|
||||||
|
GenerationTask.task_type == "video",
|
||||||
|
GenerationTask.payload["prompt"].as_string().isnot(None),
|
||||||
|
)
|
||||||
|
.order_by(GenerationTask.id.desc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(size)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
result: list[dict] = []
|
||||||
|
for task in tasks:
|
||||||
|
payload = task.payload or {}
|
||||||
|
url = task.result_url or payload.get("video_url") or payload.get("dashscope_video_url") or ""
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"id": int(task.id),
|
||||||
|
"prompt": payload.get("prompt") or "",
|
||||||
|
"status": task.status,
|
||||||
|
"video_url": _cdnize(url) or "",
|
||||||
|
"error_msg": task.error_msg,
|
||||||
|
"created_at": task.created_at.isoformat() if task.created_at else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return success_response(result, msg="获取成功")
|
||||||
|
|
||||||
|
|
||||||
class DanceGenerateIn(BaseModel):
|
class DanceGenerateIn(BaseModel):
|
||||||
prompt: str = Field(..., min_length=2, max_length=400, description="用户希望跳的舞/动作描述")
|
prompt: str = Field(..., min_length=2, max_length=400, description="用户希望跳的舞/动作描述")
|
||||||
|
|
||||||
|
|
@ -92,6 +277,41 @@ class DanceTaskStatusOut(BaseModel):
|
||||||
error_msg: Optional[str] = None
|
error_msg: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/current", response_model=ApiResponse[Optional[DanceTaskStatusOut]])
|
||||||
|
def get_current_dance_task(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: AuthedUser = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
task = (
|
||||||
|
db.query(GenerationTask)
|
||||||
|
.filter(
|
||||||
|
GenerationTask.user_id == user.id,
|
||||||
|
GenerationTask.task_type == "video",
|
||||||
|
GenerationTask.status.in_(["pending", "running"]),
|
||||||
|
GenerationTask.payload["prompt"].as_string().isnot(None),
|
||||||
|
)
|
||||||
|
.order_by(GenerationTask.id.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not task:
|
||||||
|
return success_response(None, msg="暂无进行中的任务")
|
||||||
|
|
||||||
|
payload = task.payload or {}
|
||||||
|
return success_response(
|
||||||
|
DanceTaskStatusOut(
|
||||||
|
generation_task_id=task.id,
|
||||||
|
status=task.status,
|
||||||
|
dashscope_task_id=str(payload.get("dashscope_task_id") or ""),
|
||||||
|
video_url=task.result_url or payload.get("video_url") or payload.get("dashscope_video_url") or "",
|
||||||
|
session_id=int(payload.get("session_id") or 0),
|
||||||
|
user_message_id=int(payload.get("user_message_id") or 0),
|
||||||
|
lover_message_id=int(payload.get("lover_message_id") or 0),
|
||||||
|
error_msg=task.error_msg,
|
||||||
|
),
|
||||||
|
msg="获取成功",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _upload_to_oss(file_bytes: bytes, object_name: str) -> str:
|
def _upload_to_oss(file_bytes: bytes, object_name: str) -> str:
|
||||||
if not settings.ALIYUN_OSS_ACCESS_KEY_ID or not settings.ALIYUN_OSS_ACCESS_KEY_SECRET:
|
if not settings.ALIYUN_OSS_ACCESS_KEY_ID or not settings.ALIYUN_OSS_ACCESS_KEY_SECRET:
|
||||||
raise HTTPException(status_code=500, detail="未配置 OSS Key")
|
raise HTTPException(status_code=500, detail="未配置 OSS Key")
|
||||||
|
|
@ -285,7 +505,7 @@ def _download_to_path(url: str, target_path: str, label: str):
|
||||||
|
|
||||||
def _probe_media_duration(path: str) -> Optional[float]:
|
def _probe_media_duration(path: str) -> Optional[float]:
|
||||||
command = [
|
command = [
|
||||||
"ffprobe",
|
_ffprobe_bin(),
|
||||||
"-v",
|
"-v",
|
||||||
"error",
|
"error",
|
||||||
"-show_entries",
|
"-show_entries",
|
||||||
|
|
@ -315,7 +535,7 @@ def _probe_media_duration(path: str) -> Optional[float]:
|
||||||
def _run_ffmpeg_merge(video_path: str, audio_path: str, output_path: str):
|
def _run_ffmpeg_merge(video_path: str, audio_path: str, output_path: str):
|
||||||
audio_duration = _probe_media_duration(audio_path)
|
audio_duration = _probe_media_duration(audio_path)
|
||||||
command = [
|
command = [
|
||||||
"ffmpeg",
|
_ffmpeg_bin(),
|
||||||
"-y",
|
"-y",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"error",
|
"error",
|
||||||
|
|
@ -364,7 +584,7 @@ def _extract_audio_segment(
|
||||||
output_path: str,
|
output_path: str,
|
||||||
):
|
):
|
||||||
command = [
|
command = [
|
||||||
"ffmpeg",
|
_ffmpeg_bin(),
|
||||||
"-y",
|
"-y",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"error",
|
"error",
|
||||||
|
|
@ -403,7 +623,7 @@ def _pad_audio_segment(
|
||||||
if pad_sec <= 0:
|
if pad_sec <= 0:
|
||||||
return
|
return
|
||||||
command = [
|
command = [
|
||||||
"ffmpeg",
|
_ffmpeg_bin(),
|
||||||
"-y",
|
"-y",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"error",
|
"error",
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,61 @@
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from lover.deps import get_current_user, AuthedUser
|
from lover.deps import get_current_user, AuthedUser
|
||||||
from lover.response import success_response
|
from lover.response import success_response
|
||||||
|
from lover.db import get_db
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_, or_
|
||||||
|
from lover.models import FriendRelation, User
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/api/friend/index")
|
@router.get("/api/friend/index")
|
||||||
def get_friend_index(user: AuthedUser = Depends(get_current_user)):
|
@router.post("/api/friend/index")
|
||||||
|
def get_friend_index(user: AuthedUser = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||||
"""获取好友列表"""
|
"""获取好友列表"""
|
||||||
|
# 查询当前用户的好友关系(双向:user_id 或 friend_id 为当前用户,且 status=1 表示已通过)
|
||||||
|
friend_relations = db.query(FriendRelation).filter(
|
||||||
|
and_(
|
||||||
|
or_(
|
||||||
|
FriendRelation.user_id == user.id,
|
||||||
|
FriendRelation.friend_id == user.id
|
||||||
|
),
|
||||||
|
FriendRelation.status == "1"
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
friend_ids = []
|
||||||
|
result = []
|
||||||
|
for rel in friend_relations:
|
||||||
|
# 确定好友的 user_id(不是自己)
|
||||||
|
fid = rel.friend_id if rel.user_id == user.id else rel.user_id
|
||||||
|
friend_ids.append(fid)
|
||||||
|
|
||||||
|
# 查询好友用户信息
|
||||||
|
friend_users = db.query(User).filter(User.id.in_(friend_ids)).all() if friend_ids else []
|
||||||
|
user_map = {u.id: u for u in friend_users}
|
||||||
|
|
||||||
|
for rel in friend_relations:
|
||||||
|
fid = rel.friend_id if rel.user_id == user.id else rel.user_id
|
||||||
|
friend_user = user_map.get(fid)
|
||||||
|
if not friend_user:
|
||||||
|
continue
|
||||||
|
result.append({
|
||||||
|
"friend_id": fid,
|
||||||
|
"friend": {
|
||||||
|
"id": friend_user.id,
|
||||||
|
"nickname": friend_user.nickname,
|
||||||
|
"avatar": friend_user.avatar,
|
||||||
|
"user_number": str(friend_user.id), # 前端期望 user_number
|
||||||
|
"open_id": "", # 小程序 open_id,如有需要可补充字段
|
||||||
|
},
|
||||||
|
"intimacy": rel.intimacy or 0,
|
||||||
|
"intimacy_level": rel.intimacy_level or 0,
|
||||||
|
"is_online": False, # 默认离线,前端会单独调用在线状态接口
|
||||||
|
})
|
||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
"friends": [],
|
"data": result,
|
||||||
"total": 0
|
"total": len(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
@router.post("/api/friend/add")
|
@router.post("/api/friend/add")
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from lover.deps import get_current_user, AuthedUser
|
from lover.deps import get_current_user, AuthedUser
|
||||||
from lover.response import success_response
|
from lover.response import success_response
|
||||||
|
|
@ -17,6 +19,34 @@ def register_huanxin_user(user: AuthedUser = Depends(get_current_user)):
|
||||||
"""注册环信用户"""
|
"""注册环信用户"""
|
||||||
return success_response({"message": "注册成功"})
|
return success_response({"message": "注册成功"})
|
||||||
|
|
||||||
|
@router.post("/api/huanxin/online")
|
||||||
|
def set_huanxin_online(
|
||||||
|
payload: Optional[dict] = None,
|
||||||
|
user: AuthedUser = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""获取环信用户在线状态(兼容前端批量查询)"""
|
||||||
|
payload = payload or {}
|
||||||
|
user_ids_raw = payload.get("user_ids")
|
||||||
|
|
||||||
|
# 前端好友列表会传入 user_ids=36,40,41,期待返回数组并支持 .find()
|
||||||
|
if user_ids_raw:
|
||||||
|
ids = []
|
||||||
|
for part in str(user_ids_raw).split(","):
|
||||||
|
part = part.strip()
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
ids.append(int(part))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return success_response([
|
||||||
|
{"id": uid, "is_online": False} for uid in ids
|
||||||
|
])
|
||||||
|
|
||||||
|
# 兼容旧调用:不传 user_ids 时,返回当前用户在线信息
|
||||||
|
return success_response({"status": "online", "user_id": user.id})
|
||||||
|
|
||||||
@router.get("/api/huanxin/user_info")
|
@router.get("/api/huanxin/user_info")
|
||||||
def get_huanxin_user_info(user: AuthedUser = Depends(get_current_user)):
|
def get_huanxin_user_info(user: AuthedUser = Depends(get_current_user)):
|
||||||
"""获取环信用户信息"""
|
"""获取环信用户信息"""
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import logging
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
|
|
@ -35,8 +37,48 @@ from ..models import (
|
||||||
from ..response import ApiResponse, success_response
|
from ..response import ApiResponse, success_response
|
||||||
from ..task_queue import sing_task_queue
|
from ..task_queue import sing_task_queue
|
||||||
|
|
||||||
|
try:
|
||||||
|
import imageio_ffmpeg # type: ignore
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
imageio_ffmpeg = None
|
||||||
|
|
||||||
|
logger = logging.getLogger("sing")
|
||||||
|
|
||||||
router = APIRouter(prefix="/sing", tags=["sing"])
|
router = APIRouter(prefix="/sing", tags=["sing"])
|
||||||
|
|
||||||
|
|
||||||
|
def _ffmpeg_bin() -> str:
|
||||||
|
"""Prefer system ffmpeg; fallback to imageio-ffmpeg bundled binary."""
|
||||||
|
found = shutil.which("ffmpeg")
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
if imageio_ffmpeg is not None:
|
||||||
|
try:
|
||||||
|
return imageio_ffmpeg.get_ffmpeg_exe()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "ffmpeg"
|
||||||
|
|
||||||
|
|
||||||
|
def _ffprobe_bin() -> str:
|
||||||
|
"""Prefer system ffprobe; fallback to imageio-ffmpeg bundled binary if available."""
|
||||||
|
found = shutil.which("ffprobe")
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
# imageio-ffmpeg only guarantees ffmpeg; most builds include ffprobe alongside.
|
||||||
|
if imageio_ffmpeg is not None:
|
||||||
|
try:
|
||||||
|
ffmpeg_path = imageio_ffmpeg.get_ffmpeg_exe()
|
||||||
|
candidate = os.path.join(os.path.dirname(ffmpeg_path), "ffprobe.exe")
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
return candidate
|
||||||
|
candidate = os.path.join(os.path.dirname(ffmpeg_path), "ffprobe")
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
return candidate
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "ffprobe"
|
||||||
|
|
||||||
SING_BASE_MODEL = "wan2.5-i2v-preview"
|
SING_BASE_MODEL = "wan2.5-i2v-preview"
|
||||||
SING_BASE_RESOLUTION = "480P"
|
SING_BASE_RESOLUTION = "480P"
|
||||||
SING_WAN26_MODEL = "wan2.6-i2v-flash"
|
SING_WAN26_MODEL = "wan2.6-i2v-flash"
|
||||||
|
|
@ -329,7 +371,7 @@ def _ensure_emo_detect_cache(
|
||||||
|
|
||||||
def _probe_media_duration(path: str) -> Optional[float]:
|
def _probe_media_duration(path: str) -> Optional[float]:
|
||||||
command = [
|
command = [
|
||||||
"ffprobe",
|
_ffprobe_bin(),
|
||||||
"-v",
|
"-v",
|
||||||
"error",
|
"error",
|
||||||
"-show_entries",
|
"-show_entries",
|
||||||
|
|
@ -359,7 +401,7 @@ def _probe_media_duration(path: str) -> Optional[float]:
|
||||||
def _run_ffmpeg_merge(video_path: str, audio_path: str, output_path: str):
|
def _run_ffmpeg_merge(video_path: str, audio_path: str, output_path: str):
|
||||||
audio_duration = _probe_media_duration(audio_path)
|
audio_duration = _probe_media_duration(audio_path)
|
||||||
command = [
|
command = [
|
||||||
"ffmpeg",
|
_ffmpeg_bin(),
|
||||||
"-y",
|
"-y",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"error",
|
"error",
|
||||||
|
|
@ -409,7 +451,7 @@ def _strip_video_audio(video_bytes: bytes) -> bytes:
|
||||||
with open(input_path, "wb") as file_handle:
|
with open(input_path, "wb") as file_handle:
|
||||||
file_handle.write(video_bytes)
|
file_handle.write(video_bytes)
|
||||||
command = [
|
command = [
|
||||||
"ffmpeg",
|
_ffmpeg_bin(),
|
||||||
"-y",
|
"-y",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"error",
|
"error",
|
||||||
|
|
@ -431,7 +473,7 @@ def _strip_video_audio(video_bytes: bytes) -> bytes:
|
||||||
raise HTTPException(status_code=500, detail="ffmpeg 未安装或不可用") from exc
|
raise HTTPException(status_code=500, detail="ffmpeg 未安装或不可用") from exc
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
fallback = [
|
fallback = [
|
||||||
"ffmpeg",
|
_ffmpeg_bin(),
|
||||||
"-y",
|
"-y",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"error",
|
"error",
|
||||||
|
|
@ -483,7 +525,7 @@ def _extract_audio_segment(
|
||||||
output_path: str,
|
output_path: str,
|
||||||
):
|
):
|
||||||
command = [
|
command = [
|
||||||
"ffmpeg",
|
_ffmpeg_bin(),
|
||||||
"-y",
|
"-y",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"error",
|
"error",
|
||||||
|
|
@ -523,7 +565,7 @@ def _pad_audio_segment(
|
||||||
if pad_sec <= 0:
|
if pad_sec <= 0:
|
||||||
return
|
return
|
||||||
command = [
|
command = [
|
||||||
"ffmpeg",
|
_ffmpeg_bin(),
|
||||||
"-y",
|
"-y",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"error",
|
"error",
|
||||||
|
|
@ -555,7 +597,7 @@ def _pad_audio_segment(
|
||||||
|
|
||||||
def _trim_video_duration(input_path: str, target_duration_sec: float, output_path: str):
|
def _trim_video_duration(input_path: str, target_duration_sec: float, output_path: str):
|
||||||
command = [
|
command = [
|
||||||
"ffmpeg",
|
_ffmpeg_bin(),
|
||||||
"-y",
|
"-y",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"error",
|
"error",
|
||||||
|
|
@ -579,7 +621,7 @@ def _trim_video_duration(input_path: str, target_duration_sec: float, output_pat
|
||||||
pass
|
pass
|
||||||
|
|
||||||
fallback = [
|
fallback = [
|
||||||
"ffmpeg",
|
_ffmpeg_bin(),
|
||||||
"-y",
|
"-y",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"error",
|
"error",
|
||||||
|
|
@ -632,7 +674,7 @@ def _concat_video_files(video_paths: list[str], output_path: str):
|
||||||
for path in video_paths:
|
for path in video_paths:
|
||||||
list_file.write(f"file '{path}'\n")
|
list_file.write(f"file '{path}'\n")
|
||||||
command = [
|
command = [
|
||||||
"ffmpeg",
|
_ffmpeg_bin(),
|
||||||
"-y",
|
"-y",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"error",
|
"error",
|
||||||
|
|
@ -653,7 +695,7 @@ def _concat_video_files(video_paths: list[str], output_path: str):
|
||||||
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
fallback = [
|
fallback = [
|
||||||
"ffmpeg",
|
_ffmpeg_bin(),
|
||||||
"-y",
|
"-y",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"error",
|
"error",
|
||||||
|
|
@ -771,8 +813,6 @@ def _submit_emo_video(
|
||||||
ext_bbox: list,
|
ext_bbox: list,
|
||||||
style_level: str,
|
style_level: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
import logging
|
|
||||||
logger = logging.getLogger("sing")
|
|
||||||
|
|
||||||
if not settings.DASHSCOPE_API_KEY:
|
if not settings.DASHSCOPE_API_KEY:
|
||||||
raise HTTPException(status_code=500, detail="未配置 DASHSCOPE_API_KEY")
|
raise HTTPException(status_code=500, detail="未配置 DASHSCOPE_API_KEY")
|
||||||
|
|
@ -836,9 +876,46 @@ def _submit_emo_video(
|
||||||
return str(task_id)
|
return str(task_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_task_status_in_db(dashscope_task_id: str, status: str, result_url: Optional[str] = None):
|
||||||
|
"""实时更新数据库中的任务状态"""
|
||||||
|
from lover.db import SessionLocal
|
||||||
|
from lover.models import GenerationTask
|
||||||
|
|
||||||
|
try:
|
||||||
|
with SessionLocal() as db:
|
||||||
|
# 查找对应的 GenerationTask
|
||||||
|
task = db.query(GenerationTask).filter(
|
||||||
|
GenerationTask.payload.like(f'%"dashscope_task_id": "{dashscope_task_id}"%')
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if task:
|
||||||
|
# 映射状态
|
||||||
|
status_mapping = {
|
||||||
|
"PENDING": "pending",
|
||||||
|
"RUNNING": "running",
|
||||||
|
"SUCCEEDED": "succeeded",
|
||||||
|
"FAILED": "failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
task_status = status_mapping.get(status, "pending")
|
||||||
|
task.status = task_status
|
||||||
|
|
||||||
|
if result_url:
|
||||||
|
task.result_url = result_url
|
||||||
|
|
||||||
|
# 更新 payload 中的状态
|
||||||
|
payload = task.payload or {}
|
||||||
|
payload["dashscope_status"] = status
|
||||||
|
payload["last_updated"] = time.time()
|
||||||
|
task.payload = payload
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
logger.info(f"📊 任务 {task.id} 状态已更新为: {task_status}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 更新任务状态失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _poll_video_url(task_id: str, timeout_seconds: int = 360) -> str:
|
def _poll_video_url(task_id: str, timeout_seconds: int = 360) -> str:
|
||||||
import logging
|
|
||||||
logger = logging.getLogger("sing")
|
|
||||||
logger.info(f"⏳ 开始轮询 DashScope 任务: {task_id}")
|
logger.info(f"⏳ 开始轮询 DashScope 任务: {task_id}")
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {settings.DASHSCOPE_API_KEY}"}
|
headers = {"Authorization": f"Bearer {settings.DASHSCOPE_API_KEY}"}
|
||||||
|
|
@ -872,9 +949,11 @@ def _poll_video_url(task_id: str, timeout_seconds: int = 360) -> str:
|
||||||
or ""
|
or ""
|
||||||
).upper()
|
).upper()
|
||||||
|
|
||||||
# 每 5 次(15秒)记录一次进度
|
# 每 5 次(15秒)记录一次进度并更新数据库
|
||||||
if attempts % 5 == 0:
|
if attempts % 5 == 0:
|
||||||
logger.info(f"🔄 轮询任务 {task_id} 第 {attempts} 次,状态: {status_str}")
|
logger.info(f"🔄 轮询任务 {task_id} 第 {attempts} 次,状态: {status_str}")
|
||||||
|
# 实时更新数据库状态
|
||||||
|
_update_task_status_in_db(task_id, status_str, None)
|
||||||
|
|
||||||
if status_str == "SUCCEEDED":
|
if status_str == "SUCCEEDED":
|
||||||
results = output.get("results") or {}
|
results = output.get("results") or {}
|
||||||
|
|
@ -888,6 +967,8 @@ def _poll_video_url(task_id: str, timeout_seconds: int = 360) -> str:
|
||||||
logger.error(f"❌ 任务 {task_id} 成功但未返回 URL")
|
logger.error(f"❌ 任务 {task_id} 成功但未返回 URL")
|
||||||
raise HTTPException(status_code=502, detail="视频生成成功但未返回结果 URL")
|
raise HTTPException(status_code=502, detail="视频生成成功但未返回结果 URL")
|
||||||
logger.info(f"✅ 任务 {task_id} 生成成功!")
|
logger.info(f"✅ 任务 {task_id} 生成成功!")
|
||||||
|
# 立即更新数据库状态为成功
|
||||||
|
_update_task_status_in_db(task_id, "SUCCEEDED", url)
|
||||||
return url
|
return url
|
||||||
if status_str == "FAILED":
|
if status_str == "FAILED":
|
||||||
code = output.get("code") or data.get("code")
|
code = output.get("code") or data.get("code")
|
||||||
|
|
@ -895,6 +976,8 @@ def _poll_video_url(task_id: str, timeout_seconds: int = 360) -> str:
|
||||||
if code:
|
if code:
|
||||||
msg = f"{code}: {msg}"
|
msg = f"{code}: {msg}"
|
||||||
logger.error(f"❌ 任务 {task_id} 生成失败: {msg}")
|
logger.error(f"❌ 任务 {task_id} 生成失败: {msg}")
|
||||||
|
# 立即更新数据库状态为失败
|
||||||
|
_update_task_status_in_db(task_id, "FAILED", None)
|
||||||
raise HTTPException(status_code=502, detail=f"视频生成失败: {msg}")
|
raise HTTPException(status_code=502, detail=f"视频生成失败: {msg}")
|
||||||
|
|
||||||
logger.error(f"⏱️ 任务 {task_id} 轮询超时,共尝试 {attempts} 次")
|
logger.error(f"⏱️ 任务 {task_id} 轮询超时,共尝试 {attempts} 次")
|
||||||
|
|
@ -1392,8 +1475,6 @@ def _should_enqueue_task(task_id: int) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def _enqueue_sing_task(task_id: int):
|
def _enqueue_sing_task(task_id: int):
|
||||||
import logging
|
|
||||||
logger = logging.getLogger("sing")
|
|
||||||
# 移除入队日志,只在失败时记录
|
# 移除入队日志,只在失败时记录
|
||||||
result = sing_task_queue.enqueue_unique(f"sing:{task_id}", _process_sing_task, task_id)
|
result = sing_task_queue.enqueue_unique(f"sing:{task_id}", _process_sing_task, task_id)
|
||||||
return result
|
return result
|
||||||
|
|
@ -1546,6 +1627,41 @@ class SingTaskStatusOut(BaseModel):
|
||||||
error_msg: Optional[str] = None
|
error_msg: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/current", response_model=ApiResponse[Optional[SingTaskStatusOut]])
|
||||||
|
def get_current_sing_task(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: AuthedUser = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
task = (
|
||||||
|
db.query(GenerationTask)
|
||||||
|
.filter(
|
||||||
|
GenerationTask.user_id == user.id,
|
||||||
|
GenerationTask.task_type == "video",
|
||||||
|
GenerationTask.status.in_(["pending", "running"]),
|
||||||
|
GenerationTask.payload["song_id"].as_integer().isnot(None),
|
||||||
|
)
|
||||||
|
.order_by(GenerationTask.id.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not task:
|
||||||
|
return success_response(None, msg="暂无进行中的任务")
|
||||||
|
|
||||||
|
payload = task.payload or {}
|
||||||
|
return success_response(
|
||||||
|
SingTaskStatusOut(
|
||||||
|
generation_task_id=task.id,
|
||||||
|
status=task.status,
|
||||||
|
dashscope_task_id=str(payload.get("dashscope_task_id") or ""),
|
||||||
|
video_url=task.result_url or payload.get("merged_video_url") or "",
|
||||||
|
session_id=int(payload.get("session_id") or 0),
|
||||||
|
user_message_id=int(payload.get("user_message_id") or 0),
|
||||||
|
lover_message_id=int(payload.get("lover_message_id") or 0),
|
||||||
|
error_msg=task.error_msg,
|
||||||
|
),
|
||||||
|
msg="获取成功",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/songs", response_model=ApiResponse[SongListResponse])
|
@router.get("/songs", response_model=ApiResponse[SongListResponse])
|
||||||
def list_songs_for_lover(
|
def list_songs_for_lover(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
|
@ -1590,7 +1706,7 @@ def get_sing_history(
|
||||||
if not lover:
|
if not lover:
|
||||||
raise HTTPException(status_code=404, detail="恋人未找到")
|
raise HTTPException(status_code=404, detail="恋人未找到")
|
||||||
|
|
||||||
# 查询已成功生成的视频
|
# 查询已成功生成的视频(优先使用 nf_sing_song_video)
|
||||||
offset = (page - 1) * size
|
offset = (page - 1) * size
|
||||||
videos = (
|
videos = (
|
||||||
db.query(SingSongVideo)
|
db.query(SingSongVideo)
|
||||||
|
|
@ -1606,19 +1722,162 @@ def get_sing_history(
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
result = []
|
result: list[dict] = []
|
||||||
|
seen_urls: set[str] = set()
|
||||||
for video in videos:
|
for video in videos:
|
||||||
# 获取歌曲信息
|
# 获取歌曲信息
|
||||||
song = db.query(SongLibrary).filter(SongLibrary.id == video.song_id).first()
|
song = db.query(SongLibrary).filter(SongLibrary.id == video.song_id).first()
|
||||||
song_title = song.title if song else "未知歌曲"
|
song_title = song.title if song else "未知歌曲"
|
||||||
|
url = _cdnize(video.merged_video_url)
|
||||||
|
if url:
|
||||||
|
seen_urls.add(url)
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"id": video.id,
|
||||||
|
"song_id": video.song_id,
|
||||||
|
"song_title": song_title,
|
||||||
|
"video_url": url,
|
||||||
|
"created_at": video.created_at.isoformat() if video.created_at else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
result.append({
|
# 兜底:部分情况下任务成功但 nf_sing_song_video 未落库,补查 nf_generation_tasks
|
||||||
"id": video.id,
|
if len(result) < size:
|
||||||
"song_id": video.song_id,
|
remaining = size - len(result)
|
||||||
"song_title": song_title,
|
fallback_tasks = (
|
||||||
"video_url": _cdnize(video.merged_video_url),
|
db.query(GenerationTask)
|
||||||
"created_at": video.created_at.isoformat() if video.created_at else None,
|
.filter(
|
||||||
})
|
GenerationTask.user_id == user.id,
|
||||||
|
GenerationTask.lover_id == lover.id,
|
||||||
|
GenerationTask.task_type == "video",
|
||||||
|
GenerationTask.status == "succeeded",
|
||||||
|
GenerationTask.payload["song_id"].as_integer().isnot(None),
|
||||||
|
GenerationTask.payload["merged_video_url"].as_string().isnot(None),
|
||||||
|
)
|
||||||
|
.order_by(GenerationTask.id.desc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(size * 2)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
for task in fallback_tasks:
|
||||||
|
payload = task.payload or {}
|
||||||
|
song_id = payload.get("song_id")
|
||||||
|
merged_video_url = payload.get("merged_video_url") or task.result_url
|
||||||
|
url = _cdnize(merged_video_url) if merged_video_url else ""
|
||||||
|
if not url or url in seen_urls:
|
||||||
|
continue
|
||||||
|
|
||||||
|
song_title = payload.get("song_title") or "未知歌曲"
|
||||||
|
if song_id:
|
||||||
|
song = db.query(SongLibrary).filter(SongLibrary.id == song_id).first()
|
||||||
|
if song and song.title:
|
||||||
|
song_title = song.title
|
||||||
|
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"id": int(task.id),
|
||||||
|
"song_id": int(song_id) if song_id else 0,
|
||||||
|
"song_title": song_title,
|
||||||
|
"video_url": url,
|
||||||
|
"created_at": task.created_at.isoformat() if task.created_at else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
seen_urls.add(url)
|
||||||
|
remaining -= 1
|
||||||
|
if remaining <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
return success_response(result, msg="获取成功")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/retry/{task_id}", response_model=ApiResponse[dict])
|
||||||
|
def retry_sing_task(
|
||||||
|
task_id: int,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: AuthedUser = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""手动重试:用于 DashScope 端已成功但本地下载/上传失败导致任务失败的情况。"""
|
||||||
|
task = (
|
||||||
|
db.query(GenerationTask)
|
||||||
|
.filter(
|
||||||
|
GenerationTask.id == task_id,
|
||||||
|
GenerationTask.user_id == user.id,
|
||||||
|
GenerationTask.task_type == "video",
|
||||||
|
GenerationTask.payload["song_id"].as_integer().isnot(None),
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="任务不存在")
|
||||||
|
|
||||||
|
payload = task.payload or {}
|
||||||
|
dash_id = payload.get("dashscope_task_id")
|
||||||
|
if not dash_id:
|
||||||
|
# 唱歌任务通常不会在 GenerationTask.payload 中保存 dashscope_task_id(分段任务各自保存)。
|
||||||
|
# 此时改为重新入队处理,尽量复用已成功的分段,完成补下载/补写记录。
|
||||||
|
task.status = "pending"
|
||||||
|
task.error_msg = None
|
||||||
|
task.payload = {**payload, "manual_retry": True}
|
||||||
|
task.updated_at = datetime.utcnow()
|
||||||
|
db.add(task)
|
||||||
|
db.commit()
|
||||||
|
_enqueue_sing_task(int(task.id))
|
||||||
|
return success_response({"task_id": int(task.id)}, msg="已触发重新下载")
|
||||||
|
|
||||||
|
# 标记手动重试(避免前端重复点击导致并发过多)
|
||||||
|
task.payload = {**payload, "manual_retry": True}
|
||||||
|
task.updated_at = datetime.utcnow()
|
||||||
|
db.add(task)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
background_tasks.add_task(_retry_finalize_sing_task, int(task.id))
|
||||||
|
return success_response({"task_id": int(task.id)}, msg="已触发重新下载")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/history/all", response_model=ApiResponse[List[dict]])
|
||||||
|
def get_sing_history_all(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
user: AuthedUser = Depends(get_current_user),
|
||||||
|
page: int = 1,
|
||||||
|
size: int = 20,
|
||||||
|
):
|
||||||
|
lover = db.query(Lover).filter(Lover.user_id == user.id).first()
|
||||||
|
if not lover:
|
||||||
|
raise HTTPException(status_code=404, detail="恋人未找到")
|
||||||
|
|
||||||
|
offset = (page - 1) * size
|
||||||
|
tasks = (
|
||||||
|
db.query(GenerationTask)
|
||||||
|
.filter(
|
||||||
|
GenerationTask.user_id == user.id,
|
||||||
|
GenerationTask.lover_id == lover.id,
|
||||||
|
GenerationTask.task_type == "video",
|
||||||
|
GenerationTask.payload["song_id"].as_integer().isnot(None),
|
||||||
|
)
|
||||||
|
.order_by(GenerationTask.id.desc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(size)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
result: list[dict] = []
|
||||||
|
for task in tasks:
|
||||||
|
payload = task.payload or {}
|
||||||
|
song_id = payload.get("song_id")
|
||||||
|
merged_video_url = payload.get("merged_video_url") or task.result_url
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"id": int(task.id),
|
||||||
|
"song_id": int(song_id) if song_id else 0,
|
||||||
|
"song_title": payload.get("song_title") or "未知歌曲",
|
||||||
|
"status": task.status,
|
||||||
|
"video_url": _cdnize(merged_video_url) or "",
|
||||||
|
"error_msg": task.error_msg,
|
||||||
|
"created_at": task.created_at.isoformat() if task.created_at else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return success_response(result, msg="获取成功")
|
return success_response(result, msg="获取成功")
|
||||||
|
|
||||||
|
|
@ -1669,8 +1928,6 @@ def _process_sing_task(task_id: int):
|
||||||
"""
|
"""
|
||||||
后台处理唱歌视频生成任务:分段音频 -> EMO 逐段生成 -> 拼接整曲。
|
后台处理唱歌视频生成任务:分段音频 -> EMO 逐段生成 -> 拼接整曲。
|
||||||
"""
|
"""
|
||||||
import logging
|
|
||||||
logger = logging.getLogger("sing")
|
|
||||||
logger.info(f"开始处理唱歌任务 {task_id}")
|
logger.info(f"开始处理唱歌任务 {task_id}")
|
||||||
|
|
||||||
song_title: str = ""
|
song_title: str = ""
|
||||||
|
|
@ -2247,16 +2504,12 @@ def _process_sing_task(task_id: int):
|
||||||
db.add(task_row)
|
db.add(task_row)
|
||||||
db.commit()
|
db.commit()
|
||||||
except HTTPException as exc:
|
except HTTPException as exc:
|
||||||
import logging
|
|
||||||
logger = logging.getLogger("sing")
|
|
||||||
logger.error(f"任务 {task_id} 处理失败 (HTTPException): {exc.detail if hasattr(exc, 'detail') else str(exc)}")
|
logger.error(f"任务 {task_id} 处理失败 (HTTPException): {exc.detail if hasattr(exc, 'detail') else str(exc)}")
|
||||||
try:
|
try:
|
||||||
_mark_task_failed(task_id, str(exc.detail) if hasattr(exc, "detail") else str(exc))
|
_mark_task_failed(task_id, str(exc.detail) if hasattr(exc, "detail") else str(exc))
|
||||||
except Exception as e2:
|
except Exception as e2:
|
||||||
logger.exception(f"标记任务 {task_id} 失败时出错: {e2}")
|
logger.exception(f"标记任务 {task_id} 失败时出错: {e2}")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
import logging
|
|
||||||
logger = logging.getLogger("sing")
|
|
||||||
logger.exception(f"任务 {task_id} 处理失败 (Exception): {exc}")
|
logger.exception(f"任务 {task_id} 处理失败 (Exception): {exc}")
|
||||||
try:
|
try:
|
||||||
_mark_task_failed(task_id, str(exc)[:255])
|
_mark_task_failed(task_id, str(exc)[:255])
|
||||||
|
|
@ -2271,6 +2524,9 @@ def generate_sing_video(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
user: AuthedUser = Depends(get_current_user),
|
user: AuthedUser = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
logger.info(f"🎤 收到唱歌生成请求: user_id={user.id}, song_id={payload.song_id}")
|
||||||
|
|
||||||
|
# 原有代码...
|
||||||
lover = db.query(Lover).filter(Lover.user_id == user.id).first()
|
lover = db.query(Lover).filter(Lover.user_id == user.id).first()
|
||||||
if not lover:
|
if not lover:
|
||||||
raise HTTPException(status_code=404, detail="恋人不存在,请先完成创建流程")
|
raise HTTPException(status_code=404, detail="恋人不存在,请先完成创建流程")
|
||||||
|
|
@ -2578,6 +2834,118 @@ def generate_sing_video(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_sing_history_record(db: Session, task: GenerationTask) -> None:
|
||||||
|
"""确保 nf_sing_song_video 中存在该任务对应的成功记录(用于历史列表)。"""
|
||||||
|
if not task or task.status != "succeeded":
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = task.payload or {}
|
||||||
|
song_id = payload.get("song_id")
|
||||||
|
merged_video_url = payload.get("merged_video_url") or task.result_url
|
||||||
|
if not song_id or not merged_video_url:
|
||||||
|
return
|
||||||
|
|
||||||
|
existing = (
|
||||||
|
db.query(SingSongVideo)
|
||||||
|
.filter(
|
||||||
|
SingSongVideo.generation_task_id == task.id,
|
||||||
|
SingSongVideo.user_id == task.user_id,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if existing and existing.status == "succeeded" and existing.merged_video_url:
|
||||||
|
return
|
||||||
|
|
||||||
|
audio_url = payload.get("audio_url") or ""
|
||||||
|
audio_hash = payload.get("audio_hash") or ( _hash_text(audio_url) if audio_url else "" )
|
||||||
|
image_hash = payload.get("image_hash") or ""
|
||||||
|
ratio = payload.get("ratio") or EMO_RATIO
|
||||||
|
style_level = payload.get("style_level") or EMO_STYLE_LEVEL
|
||||||
|
|
||||||
|
if not existing:
|
||||||
|
existing = SingSongVideo(
|
||||||
|
user_id=task.user_id,
|
||||||
|
lover_id=task.lover_id or 0,
|
||||||
|
song_id=int(song_id),
|
||||||
|
audio_url=audio_url or "",
|
||||||
|
audio_hash=(audio_hash or "")[:64],
|
||||||
|
image_hash=(image_hash or "")[:64] if image_hash else None,
|
||||||
|
ratio=ratio,
|
||||||
|
style_level=style_level,
|
||||||
|
merged_video_url=merged_video_url,
|
||||||
|
status="succeeded",
|
||||||
|
error_msg=None,
|
||||||
|
generation_task_id=task.id,
|
||||||
|
created_at=task.created_at or datetime.utcnow(),
|
||||||
|
updated_at=datetime.utcnow(),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
existing.song_id = int(song_id)
|
||||||
|
existing.merged_video_url = merged_video_url
|
||||||
|
existing.status = "succeeded"
|
||||||
|
existing.error_msg = None
|
||||||
|
existing.updated_at = datetime.utcnow()
|
||||||
|
existing.generation_task_id = task.id
|
||||||
|
if task.lover_id:
|
||||||
|
existing.lover_id = task.lover_id
|
||||||
|
if audio_url:
|
||||||
|
existing.audio_url = audio_url
|
||||||
|
if audio_hash:
|
||||||
|
existing.audio_hash = (audio_hash or "")[:64]
|
||||||
|
if image_hash:
|
||||||
|
existing.image_hash = (image_hash or "")[:64]
|
||||||
|
existing.ratio = ratio
|
||||||
|
existing.style_level = style_level
|
||||||
|
|
||||||
|
db.add(existing)
|
||||||
|
|
||||||
|
|
||||||
|
def _retry_finalize_sing_task(task_id: int) -> None:
|
||||||
|
"""任务失败但 DashScope 端已成功时,尝试重新下载视频并落库(自愈)。"""
|
||||||
|
try:
|
||||||
|
with SessionLocal() as db:
|
||||||
|
task = (
|
||||||
|
db.query(GenerationTask)
|
||||||
|
.filter(GenerationTask.id == task_id)
|
||||||
|
.with_for_update()
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not task:
|
||||||
|
return
|
||||||
|
payload = task.payload or {}
|
||||||
|
dash_id = payload.get("dashscope_task_id")
|
||||||
|
if not dash_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
status, dash_video_url, error_msg = _query_dashscope_task_status(dash_id)
|
||||||
|
if status != "SUCCEEDED" or not dash_video_url:
|
||||||
|
return
|
||||||
|
|
||||||
|
video_bytes = _download_binary(dash_video_url)
|
||||||
|
object_name = (
|
||||||
|
f"lover/{task.lover_id}/sing/"
|
||||||
|
f"{int(time.time())}_{payload.get('song_id') or 'unknown'}.mp4"
|
||||||
|
)
|
||||||
|
merged_video_url = _upload_to_oss(video_bytes, object_name)
|
||||||
|
|
||||||
|
task.status = "succeeded"
|
||||||
|
task.result_url = merged_video_url
|
||||||
|
task.error_msg = None
|
||||||
|
task.payload = {
|
||||||
|
**payload,
|
||||||
|
"merged_video_url": merged_video_url,
|
||||||
|
"retry_finalized": True,
|
||||||
|
"dashscope_error": error_msg,
|
||||||
|
}
|
||||||
|
task.updated_at = datetime.utcnow()
|
||||||
|
db.add(task)
|
||||||
|
|
||||||
|
_ensure_sing_history_record(db, task)
|
||||||
|
db.commit()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
@router.get("/generate/{task_id}", response_model=ApiResponse[SingTaskStatusOut])
|
@router.get("/generate/{task_id}", response_model=ApiResponse[SingTaskStatusOut])
|
||||||
def get_sing_task(
|
def get_sing_task(
|
||||||
task_id: int,
|
task_id: int,
|
||||||
|
|
@ -2612,11 +2980,7 @@ def get_sing_task(
|
||||||
merge_id = payload.get("merge_id")
|
merge_id = payload.get("merge_id")
|
||||||
if merge_id:
|
if merge_id:
|
||||||
with SessionLocal() as tmp:
|
with SessionLocal() as tmp:
|
||||||
merge = (
|
merge = tmp.query(SingSongVideo).filter(SingSongVideo.id == merge_id).first()
|
||||||
tmp.query(SingSongVideo)
|
|
||||||
.filter(SingSongVideo.id == merge_id)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if merge and merge.status == "succeeded" and merge.merged_video_url:
|
if merge and merge.status == "succeeded" and merge.merged_video_url:
|
||||||
current = (
|
current = (
|
||||||
tmp.query(GenerationTask)
|
tmp.query(GenerationTask)
|
||||||
|
|
@ -2636,7 +3000,6 @@ def get_sing_task(
|
||||||
"content_safety_blocked": content_safety_blocked,
|
"content_safety_blocked": content_safety_blocked,
|
||||||
}
|
}
|
||||||
current.updated_at = datetime.utcnow()
|
current.updated_at = datetime.utcnow()
|
||||||
# 更新聊天占位消息与扣减(若未扣)
|
|
||||||
try:
|
try:
|
||||||
lover_msg_id = (current.payload or {}).get("lover_message_id")
|
lover_msg_id = (current.payload or {}).get("lover_message_id")
|
||||||
session_id = (current.payload or {}).get("session_id")
|
session_id = (current.payload or {}).get("session_id")
|
||||||
|
|
@ -2688,8 +3051,21 @@ def get_sing_task(
|
||||||
tmp.commit()
|
tmp.commit()
|
||||||
task = current
|
task = current
|
||||||
|
|
||||||
resp_msg = status_msg_map.get(task.status or "", resp_msg)
|
# 自愈:成功任务但历史缺记录,补写 nf_sing_song_video
|
||||||
|
if task.status == "succeeded":
|
||||||
|
try:
|
||||||
|
_ensure_sing_history_record(db, task)
|
||||||
|
db.commit()
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
|
||||||
|
# 自愈:失败任务但 DashScope 可能已成功(下载/上传失败导致),后台重试一次
|
||||||
|
if task.status == "failed":
|
||||||
|
payload = task.payload or {}
|
||||||
|
if payload.get("dashscope_task_id") and not payload.get("retry_finalized"):
|
||||||
|
background_tasks.add_task(_retry_finalize_sing_task, int(task.id))
|
||||||
|
|
||||||
|
resp_msg = status_msg_map.get(task.status or "", resp_msg)
|
||||||
payload = task.payload or {}
|
payload = task.payload or {}
|
||||||
return success_response(
|
return success_response(
|
||||||
SingTaskStatusOut(
|
SingTaskStatusOut(
|
||||||
|
|
@ -2704,3 +3080,4 @@ def get_sing_task(
|
||||||
),
|
),
|
||||||
msg=resp_msg,
|
msg=resp_msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ def get_user_basic(user: AuthedUser = Depends(get_current_user)):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@router.get("/user_basic/get_hobbies")
|
@router.get("/api/user_basic/get_hobbies")
|
||||||
def get_hobbies(user: AuthedUser = Depends(get_current_user)):
|
def get_hobbies(user: AuthedUser = Depends(get_current_user)):
|
||||||
"""获取用户兴趣爱好"""
|
"""获取用户兴趣爱好"""
|
||||||
return success_response([
|
return success_response([
|
||||||
|
|
|
||||||
|
|
@ -115,11 +115,13 @@ export default {
|
||||||
},
|
},
|
||||||
async friend() {
|
async friend() {
|
||||||
const res = await Friend(this.form)
|
const res = await Friend(this.form)
|
||||||
|
console.log('Friend 接口返回:', res)
|
||||||
let data = ''
|
let data = ''
|
||||||
let ids = ''
|
let ids = ''
|
||||||
let onlineData = ''
|
let onlineData = ''
|
||||||
if (res.code == 1) {
|
if (res.code == 1) {
|
||||||
data = res.data.data
|
data = res.data.data || []
|
||||||
|
console.log('解析后的 data:', data)
|
||||||
ids = data.map(item => {
|
ids = data.map(item => {
|
||||||
return item.friend_id
|
return item.friend_id
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,10 @@
|
||||||
<scroll-view class="feature-scroll" scroll-y="true">
|
<scroll-view class="feature-scroll" scroll-y="true">
|
||||||
<!-- 历史视频 -->
|
<!-- 历史视频 -->
|
||||||
<view class="history-section" v-if="singHistoryList.length > 0">
|
<view class="history-section" v-if="singHistoryList.length > 0">
|
||||||
<view class="section-title" @click="openHistoryModal('sing')">历史视频</view>
|
<view class="history-section-header">
|
||||||
|
<view class="section-title">历史视频</view>
|
||||||
|
<view class="section-more" @click="openHistoryModal('sing', 'all')">查看全部</view>
|
||||||
|
</view>
|
||||||
<view class="history-list">
|
<view class="history-list">
|
||||||
<view class="history-item" v-for="(item,index) in singHistoryList.slice(0, 2)" :key="'history-'+index">
|
<view class="history-item" v-for="(item,index) in singHistoryList.slice(0, 2)" :key="'history-'+index">
|
||||||
<view class="history-info">
|
<view class="history-info">
|
||||||
|
|
@ -233,7 +236,10 @@
|
||||||
<view class="feature-desc">让她为你跳一支舞</view>
|
<view class="feature-desc">让她为你跳一支舞</view>
|
||||||
<scroll-view class="feature-scroll" scroll-y="true">
|
<scroll-view class="feature-scroll" scroll-y="true">
|
||||||
<view class="history-section" v-if="danceHistoryList.length > 0">
|
<view class="history-section" v-if="danceHistoryList.length > 0">
|
||||||
<view class="section-title" @click="openHistoryModal('dance')">历史视频</view>
|
<view class="history-section-header">
|
||||||
|
<view class="section-title">历史视频</view>
|
||||||
|
<view class="section-more" @click="openHistoryModal('dance', 'all')">查看全部</view>
|
||||||
|
</view>
|
||||||
<view class="history-list">
|
<view class="history-list">
|
||||||
<view class="history-item" v-for="(item,index) in danceHistoryList.slice(0, 2)" :key="'dance-history-'+index">
|
<view class="history-item" v-for="(item,index) in danceHistoryList.slice(0, 2)" :key="'dance-history-'+index">
|
||||||
<view class="history-info">
|
<view class="history-info">
|
||||||
|
|
@ -358,7 +364,7 @@
|
||||||
<view v-if="historyModalVisible" class="modal-mask" @click="closeHistoryModal">
|
<view v-if="historyModalVisible" class="modal-mask" @click="closeHistoryModal">
|
||||||
<view class="modal-card" @click.stop>
|
<view class="modal-card" @click.stop>
|
||||||
<view class="modal-header">
|
<view class="modal-header">
|
||||||
<view class="modal-title">{{ historyModalType === 'dance' ? '跳舞历史视频' : '唱歌历史视频' }}</view>
|
<view class="modal-title">{{ historyModalType === 'dance' ? (historyModalMode === 'all' ? '跳舞全部历史' : '跳舞历史视频') : (historyModalMode === 'all' ? '唱歌全部历史' : '唱歌历史视频') }}</view>
|
||||||
<view class="modal-close" @click="closeHistoryModal">关闭</view>
|
<view class="modal-close" @click="closeHistoryModal">关闭</view>
|
||||||
</view>
|
</view>
|
||||||
<scroll-view class="modal-list" scroll-y="true">
|
<scroll-view class="modal-list" scroll-y="true">
|
||||||
|
|
@ -366,9 +372,11 @@
|
||||||
<view class="modal-item-info">
|
<view class="modal-item-info">
|
||||||
<text class="modal-item-title">{{ historyModalType === 'dance' ? (item.prompt || '跳舞视频') : (item.song_title || '唱歌视频') }}</text>
|
<text class="modal-item-title">{{ historyModalType === 'dance' ? (item.prompt || '跳舞视频') : (item.song_title || '唱歌视频') }}</text>
|
||||||
<text class="modal-item-date">{{ formatDate(item.created_at) }}</text>
|
<text class="modal-item-date">{{ formatDate(item.created_at) }}</text>
|
||||||
|
<text v-if="historyModalMode === 'all'" class="modal-item-status">{{ formatHistoryStatus(item) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="modal-item-actions">
|
<view class="modal-item-actions">
|
||||||
<view class="modal-play" @click="openVideoPlayer(item.video_url)">播放</view>
|
<view v-if="item.video_url" class="modal-play" @click="openVideoPlayer(item.video_url)">播放</view>
|
||||||
|
<view v-if="historyModalMode === 'all' && item.status === 'failed'" class="modal-retry" @click="retryHistoryItem(item)">重新下载</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
@ -384,6 +392,20 @@
|
||||||
<video class="modal-video" :src="videoPlayerUrl" controls></video>
|
<video class="modal-video" :src="videoPlayerUrl" controls></video>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="singGenerating && currentTab === 2" class="global-block-mask" @touchmove.stop.prevent>
|
||||||
|
<view class="global-block-card">
|
||||||
|
<view class="global-block-title">正在生成视频中</view>
|
||||||
|
<view class="global-block-sub">预计等待15分钟</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="danceGenerating && currentTab === 3" class="global-block-mask" @touchmove.stop.prevent>
|
||||||
|
<view class="global-block-card">
|
||||||
|
<view class="global-block-title">正在生成视频中</view>
|
||||||
|
<view class="global-block-sub">预计等待15分钟</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -395,7 +417,10 @@
|
||||||
SingSongs,
|
SingSongs,
|
||||||
SingGenerate,
|
SingGenerate,
|
||||||
SingGenerateTask,
|
SingGenerateTask,
|
||||||
|
SingCurrent,
|
||||||
DanceGenerate,
|
DanceGenerate,
|
||||||
|
DanceGenerateTask,
|
||||||
|
DanceCurrent,
|
||||||
SessionInit,
|
SessionInit,
|
||||||
SessionSend
|
SessionSend
|
||||||
} from '@/utils/api.js'
|
} from '@/utils/api.js'
|
||||||
|
|
@ -445,6 +470,9 @@
|
||||||
danceHistoryList: [],
|
danceHistoryList: [],
|
||||||
songId: 0,
|
songId: 0,
|
||||||
singGenerating: false, // 唱歌视频生成中状态
|
singGenerating: false, // 唱歌视频生成中状态
|
||||||
|
singGeneratingTaskId: 0,
|
||||||
|
danceGenerating: false, // 跳舞视频生成中状态
|
||||||
|
danceGeneratingTaskId: 0,
|
||||||
statusBarHeight: uni.getWindowInfo().statusBarHeight,
|
statusBarHeight: uni.getWindowInfo().statusBarHeight,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
chartData: {},
|
chartData: {},
|
||||||
|
|
@ -481,6 +509,7 @@
|
||||||
underAgeEnabled: false, // 添加青少年模式状态变量
|
underAgeEnabled: false, // 添加青少年模式状态变量
|
||||||
historyModalVisible: false,
|
historyModalVisible: false,
|
||||||
historyModalType: 'sing',
|
historyModalType: 'sing',
|
||||||
|
historyModalMode: 'preview',
|
||||||
historyModalList: [],
|
historyModalList: [],
|
||||||
videoPlayerVisible: false,
|
videoPlayerVisible: false,
|
||||||
videoPlayerUrl: '',
|
videoPlayerUrl: '',
|
||||||
|
|
@ -515,6 +544,12 @@
|
||||||
// 获取歌曲列表
|
// 获取歌曲列表
|
||||||
this.getSingSongs();
|
this.getSingSongs();
|
||||||
this.getDanceHistory();
|
this.getDanceHistory();
|
||||||
|
if (this.currentTab === 2) {
|
||||||
|
this.restoreSingGeneration();
|
||||||
|
}
|
||||||
|
if (this.currentTab === 3) {
|
||||||
|
this.restoreDanceGeneration();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// Tab 切换方法
|
// Tab 切换方法
|
||||||
|
|
@ -534,6 +569,12 @@
|
||||||
|
|
||||||
this.currentTab = index;
|
this.currentTab = index;
|
||||||
this.updateTabIntoView(index);
|
this.updateTabIntoView(index);
|
||||||
|
if (index === 2) {
|
||||||
|
this.restoreSingGeneration();
|
||||||
|
}
|
||||||
|
if (index === 3) {
|
||||||
|
this.restoreDanceGeneration();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onSwiperChange(e) {
|
onSwiperChange(e) {
|
||||||
console.log('Swiper 滑动,当前索引:', e.detail.current, '对应 Tab:', this.tabs[e.detail.current].name);
|
console.log('Swiper 滑动,当前索引:', e.detail.current, '对应 Tab:', this.tabs[e.detail.current].name);
|
||||||
|
|
@ -544,6 +585,12 @@
|
||||||
if (e.detail.current === 1 && !this.chatSessionId) {
|
if (e.detail.current === 1 && !this.chatSessionId) {
|
||||||
this.initChatSession();
|
this.initChatSession();
|
||||||
}
|
}
|
||||||
|
if (e.detail.current === 2) {
|
||||||
|
this.restoreSingGeneration();
|
||||||
|
}
|
||||||
|
if (e.detail.current === 3) {
|
||||||
|
this.restoreDanceGeneration();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updateTabIntoView(index) {
|
updateTabIntoView(index) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|
@ -564,44 +611,120 @@
|
||||||
|
|
||||||
this.songId = song.id;
|
this.songId = song.id;
|
||||||
this.singGenerating = true;
|
this.singGenerating = true;
|
||||||
uni.showLoading({
|
this.singGeneratingTaskId = 0;
|
||||||
title: '正在生成视频...',
|
uni.setStorageSync('singGeneratingTaskId', 0);
|
||||||
mask: true // 添加遮罩层防止用户操作
|
uni.setStorageSync('singGenerating', true);
|
||||||
});
|
|
||||||
|
|
||||||
SingGenerate({ song_id: song.id }).then(res => {
|
SingGenerate({ song_id: song.id }).then(res => {
|
||||||
if (res.code == 1) {
|
if (res.code == 1) {
|
||||||
|
this.singGeneratingTaskId = res.data.generation_task_id;
|
||||||
|
uni.setStorageSync('singGeneratingTaskId', this.singGeneratingTaskId);
|
||||||
this.getSingGenerateTask(res.data.generation_task_id);
|
this.getSingGenerateTask(res.data.generation_task_id);
|
||||||
} else {
|
} else {
|
||||||
this.singGenerating = false;
|
this.singGenerating = false;
|
||||||
uni.hideLoading();
|
this.singGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('singGenerating', false);
|
||||||
|
uni.setStorageSync('singGeneratingTaskId', 0);
|
||||||
uni.showToast({ title: res.msg, icon: 'none' });
|
uni.showToast({ title: res.msg, icon: 'none' });
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
this.singGenerating = false;
|
this.singGenerating = false;
|
||||||
uni.hideLoading();
|
this.singGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('singGenerating', false);
|
||||||
|
uni.setStorageSync('singGeneratingTaskId', 0);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '请求失败,请重试',
|
title: '请求失败,请重试',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
restoreSingGeneration() {
|
||||||
|
if (this.currentTab !== 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!uni.getStorageSync('token')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.singGenerating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const storedTaskId = parseInt(uni.getStorageSync('singGeneratingTaskId') || 0);
|
||||||
|
const storedFlag = !!uni.getStorageSync('singGenerating');
|
||||||
|
if (storedFlag && storedTaskId > 0) {
|
||||||
|
this.singGenerating = true;
|
||||||
|
this.singGeneratingTaskId = storedTaskId;
|
||||||
|
this.getSingGenerateTask(storedTaskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SingCurrent({}).then(res => {
|
||||||
|
if (res && res.code == 1 && res.data && res.data.generation_task_id) {
|
||||||
|
this.singGenerating = true;
|
||||||
|
this.singGeneratingTaskId = res.data.generation_task_id;
|
||||||
|
uni.setStorageSync('singGenerating', true);
|
||||||
|
uni.setStorageSync('singGeneratingTaskId', this.singGeneratingTaskId);
|
||||||
|
this.getSingGenerateTask(this.singGeneratingTaskId);
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
restoreDanceGeneration() {
|
||||||
|
if (this.currentTab !== 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!uni.getStorageSync('token')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.danceGenerating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const storedTaskId = parseInt(uni.getStorageSync('danceGeneratingTaskId') || 0);
|
||||||
|
const storedFlag = !!uni.getStorageSync('danceGenerating');
|
||||||
|
if (storedFlag && storedTaskId > 0) {
|
||||||
|
this.danceGenerating = true;
|
||||||
|
this.danceGeneratingTaskId = storedTaskId;
|
||||||
|
this.getDanceGenerateTask(storedTaskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DanceCurrent({}).then(res => {
|
||||||
|
if (res && res.code == 1 && res.data && res.data.generation_task_id) {
|
||||||
|
this.danceGenerating = true;
|
||||||
|
this.danceGeneratingTaskId = res.data.generation_task_id;
|
||||||
|
uni.setStorageSync('danceGenerating', true);
|
||||||
|
uni.setStorageSync('danceGeneratingTaskId', this.danceGeneratingTaskId);
|
||||||
|
this.getDanceGenerateTask(this.danceGeneratingTaskId);
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
// 请求跳舞
|
// 请求跳舞
|
||||||
requestDance() {
|
requestDance() {
|
||||||
if (!this.dancePrompt || !this.dancePrompt.trim()) {
|
if (!this.dancePrompt || !this.dancePrompt.trim()) {
|
||||||
uni.showToast({ title: '请输入舞蹈描述', icon: 'none' });
|
this.dancePrompt = '跳一段可爱的舞蹈';
|
||||||
|
}
|
||||||
|
if (this.danceGenerating) {
|
||||||
|
uni.showToast({ title: '视频生成中,请稍候...', icon: 'none', duration: 2000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
uni.showLoading({ title: '生成中...' });
|
this.danceGenerating = true;
|
||||||
DanceGenerate({ prompt: this.dancePrompt }).then(res => {
|
this.danceGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('danceGeneratingTaskId', 0);
|
||||||
|
uni.setStorageSync('danceGenerating', true);
|
||||||
|
DanceGenerate({ prompt: this.dancePrompt.trim() }).then(res => {
|
||||||
if (res.code == 1) {
|
if (res.code == 1) {
|
||||||
uni.hideLoading();
|
this.danceGeneratingTaskId = res.data.generation_task_id;
|
||||||
uni.showToast({ title: '生成成功', icon: 'success' });
|
uni.setStorageSync('danceGeneratingTaskId', this.danceGeneratingTaskId);
|
||||||
this.getDanceHistory();
|
this.getDanceGenerateTask(this.danceGeneratingTaskId);
|
||||||
} else {
|
} else {
|
||||||
uni.hideLoading();
|
this.danceGenerating = false;
|
||||||
|
this.danceGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('danceGenerating', false);
|
||||||
|
uni.setStorageSync('danceGeneratingTaskId', 0);
|
||||||
uni.showToast({ title: res.msg, icon: 'none' });
|
uni.showToast({ title: res.msg, icon: 'none' });
|
||||||
}
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.danceGenerating = false;
|
||||||
|
this.danceGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('danceGenerating', false);
|
||||||
|
uni.setStorageSync('danceGeneratingTaskId', 0);
|
||||||
|
uni.showToast({ title: '请求失败,请重试', icon: 'none' });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// 打开商城外部链接
|
// 打开商城外部链接
|
||||||
|
|
@ -677,11 +800,75 @@
|
||||||
playDanceVideo(videoUrl) {
|
playDanceVideo(videoUrl) {
|
||||||
this.openVideoPlayer(videoUrl);
|
this.openVideoPlayer(videoUrl);
|
||||||
},
|
},
|
||||||
openHistoryModal(type) {
|
openHistoryModal(type, mode = 'preview') {
|
||||||
this.historyModalType = type;
|
this.historyModalType = type;
|
||||||
|
this.historyModalMode = mode;
|
||||||
|
if (mode === 'all') {
|
||||||
|
this.fetchAllHistory(type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.historyModalList = type === 'dance' ? (this.danceHistoryList || []) : (this.singHistoryList || []);
|
this.historyModalList = type === 'dance' ? (this.danceHistoryList || []) : (this.singHistoryList || []);
|
||||||
this.historyModalVisible = true;
|
this.historyModalVisible = true;
|
||||||
},
|
},
|
||||||
|
fetchAllHistory(type) {
|
||||||
|
const endpoint = type === 'dance' ? '/dance/history/all' : '/sing/history/all';
|
||||||
|
uni.request({
|
||||||
|
url: baseURLPy + endpoint,
|
||||||
|
method: 'GET',
|
||||||
|
header: {
|
||||||
|
'Authorization': 'Bearer ' + uni.getStorageSync("token")
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
if (res.data && res.data.code === 1 && res.data.data) {
|
||||||
|
this.historyModalList = res.data.data;
|
||||||
|
} else {
|
||||||
|
this.historyModalList = [];
|
||||||
|
}
|
||||||
|
this.historyModalVisible = true;
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
this.historyModalList = [];
|
||||||
|
this.historyModalVisible = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isDownloadFailed(item) {
|
||||||
|
const msg = (item && item.error_msg ? String(item.error_msg) : '') || '';
|
||||||
|
return msg.indexOf('下载失败') !== -1;
|
||||||
|
},
|
||||||
|
formatHistoryStatus(item) {
|
||||||
|
if (!item) return '';
|
||||||
|
const status = item.status || '';
|
||||||
|
if (status === 'succeeded') return '成功';
|
||||||
|
if (status === 'pending' || status === 'running') return '生成中';
|
||||||
|
if (status === 'failed') {
|
||||||
|
if (this.isDownloadFailed(item)) return '文件下载失败';
|
||||||
|
return '生成失败';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
retryHistoryItem(item) {
|
||||||
|
if (!item || !item.id) return;
|
||||||
|
const id = item.id;
|
||||||
|
const endpoint = this.historyModalType === 'dance' ? ('/dance/retry/' + id) : ('/sing/retry/' + id);
|
||||||
|
uni.showLoading({ title: '处理中...', mask: true });
|
||||||
|
uni.request({
|
||||||
|
url: baseURLPy + endpoint,
|
||||||
|
method: 'POST',
|
||||||
|
header: {
|
||||||
|
'Authorization': 'Bearer ' + uni.getStorageSync("token")
|
||||||
|
},
|
||||||
|
success: () => {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: '已触发重新下载', icon: 'none', duration: 1500 });
|
||||||
|
this.fetchAllHistory(this.historyModalType);
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: '重试失败', icon: 'none', duration: 1500 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
closeHistoryModal() {
|
closeHistoryModal() {
|
||||||
this.historyModalVisible = false;
|
this.historyModalVisible = false;
|
||||||
},
|
},
|
||||||
|
|
@ -743,13 +930,15 @@
|
||||||
getSingGenerateTask(task_id) {
|
getSingGenerateTask(task_id) {
|
||||||
const that = this;
|
const that = this;
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
const maxAttempts = 60; // 增加到60次,每次4秒,总共4分钟
|
const maxAttempts = 225; // 15分钟左右 (225*4s)
|
||||||
|
|
||||||
const doPoll = () => {
|
const doPoll = () => {
|
||||||
attempts++;
|
attempts++;
|
||||||
if (attempts > maxAttempts) {
|
if (attempts > maxAttempts) {
|
||||||
that.singGenerating = false;
|
that.singGenerating = false;
|
||||||
uni.hideLoading();
|
that.singGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('singGenerating', false);
|
||||||
|
uni.setStorageSync('singGeneratingTaskId', 0);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '处理超时,请稍后查看',
|
title: '处理超时,请稍后查看',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
|
|
@ -758,18 +947,14 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新加载提示
|
|
||||||
uni.showLoading({
|
|
||||||
title: `生成中... (${attempts}/${maxAttempts})`,
|
|
||||||
mask: true
|
|
||||||
});
|
|
||||||
|
|
||||||
SingGenerateTask(task_id).then(res => {
|
SingGenerateTask(task_id).then(res => {
|
||||||
if (res.code == 1) {
|
if (res.code == 1) {
|
||||||
const data = res.data;
|
const data = res.data;
|
||||||
if (data.status == 'succeeded') {
|
if (data.status == 'succeeded') {
|
||||||
that.singGenerating = false;
|
that.singGenerating = false;
|
||||||
uni.hideLoading();
|
that.singGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('singGenerating', false);
|
||||||
|
uni.setStorageSync('singGeneratingTaskId', 0);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '生成成功!',
|
title: '生成成功!',
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
|
|
@ -778,7 +963,9 @@
|
||||||
// 可以在这里刷新聊天消息或显示视频
|
// 可以在这里刷新聊天消息或显示视频
|
||||||
} else if (data.status == 'failed') {
|
} else if (data.status == 'failed') {
|
||||||
that.singGenerating = false;
|
that.singGenerating = false;
|
||||||
uni.hideLoading();
|
that.singGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('singGenerating', false);
|
||||||
|
uni.setStorageSync('singGeneratingTaskId', 0);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: data.error_msg || '生成失败',
|
title: data.error_msg || '生成失败',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
|
|
@ -786,11 +973,17 @@
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 继续轮询
|
// 继续轮询
|
||||||
|
that.singGenerating = true;
|
||||||
|
that.singGeneratingTaskId = task_id;
|
||||||
|
uni.setStorageSync('singGenerating', true);
|
||||||
|
uni.setStorageSync('singGeneratingTaskId', task_id);
|
||||||
setTimeout(doPoll, 4000);
|
setTimeout(doPoll, 4000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
that.singGenerating = false;
|
that.singGenerating = false;
|
||||||
uni.hideLoading();
|
that.singGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('singGenerating', false);
|
||||||
|
uni.setStorageSync('singGeneratingTaskId', 0);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: res.msg,
|
title: res.msg,
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
|
|
@ -799,7 +992,9 @@
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
that.singGenerating = false;
|
that.singGenerating = false;
|
||||||
uni.hideLoading();
|
that.singGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('singGenerating', false);
|
||||||
|
uni.setStorageSync('singGeneratingTaskId', 0);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '查询失败,请重试',
|
title: '查询失败,请重试',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
|
|
@ -810,6 +1005,60 @@
|
||||||
|
|
||||||
doPoll();
|
doPoll();
|
||||||
},
|
},
|
||||||
|
getDanceGenerateTask(task_id) {
|
||||||
|
const that = this;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 225; // 15分钟左右 (225*4s)
|
||||||
|
const doPoll = () => {
|
||||||
|
attempts++;
|
||||||
|
if (attempts > maxAttempts) {
|
||||||
|
that.danceGenerating = false;
|
||||||
|
that.danceGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('danceGenerating', false);
|
||||||
|
uni.setStorageSync('danceGeneratingTaskId', 0);
|
||||||
|
uni.showToast({ title: '处理超时,请稍后查看', icon: 'none', duration: 2000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DanceGenerateTask(task_id).then(res => {
|
||||||
|
if (res.code == 1) {
|
||||||
|
const data = res.data;
|
||||||
|
if (data.status == 'succeeded') {
|
||||||
|
that.danceGenerating = false;
|
||||||
|
that.danceGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('danceGenerating', false);
|
||||||
|
uni.setStorageSync('danceGeneratingTaskId', 0);
|
||||||
|
uni.showToast({ title: '生成成功!', icon: 'success', duration: 2000 });
|
||||||
|
that.getDanceHistory();
|
||||||
|
} else if (data.status == 'failed') {
|
||||||
|
that.danceGenerating = false;
|
||||||
|
that.danceGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('danceGenerating', false);
|
||||||
|
uni.setStorageSync('danceGeneratingTaskId', 0);
|
||||||
|
uni.showToast({ title: data.error_msg || '生成失败', icon: 'none', duration: 2000 });
|
||||||
|
} else {
|
||||||
|
that.danceGenerating = true;
|
||||||
|
that.danceGeneratingTaskId = task_id;
|
||||||
|
uni.setStorageSync('danceGenerating', true);
|
||||||
|
uni.setStorageSync('danceGeneratingTaskId', task_id);
|
||||||
|
setTimeout(doPoll, 4000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
that.danceGenerating = false;
|
||||||
|
that.danceGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('danceGenerating', false);
|
||||||
|
uni.setStorageSync('danceGeneratingTaskId', 0);
|
||||||
|
uni.showToast({ title: res.msg, icon: 'none', duration: 2000 });
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
that.danceGenerating = false;
|
||||||
|
that.danceGeneratingTaskId = 0;
|
||||||
|
uni.setStorageSync('danceGenerating', false);
|
||||||
|
uni.setStorageSync('danceGeneratingTaskId', 0);
|
||||||
|
uni.showToast({ title: '查询失败,请重试', icon: 'none', duration: 2000 });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
doPoll();
|
||||||
|
},
|
||||||
// 处理青少年模式状态改变事件
|
// 处理青少年模式状态改变事件
|
||||||
handleUnderAgeStatusChange(status) {
|
handleUnderAgeStatusChange(status) {
|
||||||
this.underAgeEnabled = status;
|
this.underAgeEnabled = status;
|
||||||
|
|
@ -1269,6 +1518,41 @@
|
||||||
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%), #FFFFFF;
|
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%), #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.global-block-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: calc(80rpx + var(--status-bar-height));
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
z-index: 900;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-block-card {
|
||||||
|
width: 520rpx;
|
||||||
|
padding: 36rpx 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-block-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111111;
|
||||||
|
line-height: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-block-sub {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 38rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.backA {
|
.backA {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -1696,12 +1980,28 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 0;
|
||||||
padding-left: 10rpx;
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-more {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #0053FA;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: rgba(0, 83, 250, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-list {
|
.history-list {
|
||||||
|
|
@ -1806,6 +2106,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 50rpx;
|
border-radius: 50rpx;
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 商城列表样式 */
|
/* 商城列表样式 */
|
||||||
|
|
@ -2429,20 +2730,31 @@
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.modal-header {
|
.modal-header {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
padding: 24rpx;
|
padding: 24rpx;
|
||||||
border-bottom: 1px solid rgba(0,0,0,0.06);
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||||
}
|
}
|
||||||
.modal-title {
|
.modal-title {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
max-width: 70%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
}
|
}
|
||||||
.modal-close {
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 24rpx;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #666666;
|
color: #666666;
|
||||||
padding: 6rpx 12rpx;
|
padding: 6rpx 12rpx;
|
||||||
|
|
@ -2450,11 +2762,13 @@
|
||||||
.modal-list {
|
.modal-list {
|
||||||
max-height: 60vh;
|
max-height: 60vh;
|
||||||
padding: 12rpx 24rpx 24rpx;
|
padding: 12rpx 24rpx 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.modal-item {
|
.modal-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
|
gap: 12rpx;
|
||||||
padding: 16rpx 0;
|
padding: 16rpx 0;
|
||||||
border-bottom: 1px solid rgba(0,0,0,0.06);
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||||
}
|
}
|
||||||
|
|
@ -2462,29 +2776,65 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
padding-right: 12rpx;
|
padding-right: 12rpx;
|
||||||
}
|
}
|
||||||
.modal-item-title {
|
.modal-item-title {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.modal-retry {
|
||||||
|
min-width: 112rpx;
|
||||||
|
max-width: 140rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
background: #1DB954;
|
||||||
|
padding: 10rpx 16rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-left: 12rpx;
|
||||||
}
|
}
|
||||||
.modal-item-date {
|
.modal-item-date {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
margin-top: 6rpx;
|
margin-top: 6rpx;
|
||||||
}
|
}
|
||||||
|
.modal-item-status {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #666666;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.modal-item-actions {
|
.modal-item-actions {
|
||||||
width: 120rpx;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 12rpx;
|
||||||
}
|
}
|
||||||
.modal-play {
|
.modal-play {
|
||||||
|
min-width: 112rpx;
|
||||||
|
max-width: 140rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background: #F661B5;
|
background: #F661B5;
|
||||||
padding: 10rpx 16rpx;
|
padding: 10rpx 16rpx;
|
||||||
border-radius: 12rpx;
|
border-radius: 12rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.modal-video {
|
.modal-video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
request
|
request
|
||||||
} from '@/utils/request.js'
|
} from '@/utils/request.js'
|
||||||
|
|
@ -9,18 +7,18 @@ export const getTokenApi = (data) => request({
|
||||||
url: '/api/huanxin/getToken',
|
url: '/api/huanxin/getToken',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
})
|
}, 2) // 使用 FastAPI
|
||||||
|
|
||||||
// 获取环信在线
|
// 获取环信在线
|
||||||
export const getOnlineApi = (data) => request({
|
export const getOnlineApi = (data) => request({
|
||||||
url: '/api/huanxin/online',
|
url: '/api/huanxin/online',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
})
|
}, 2) // 使用 FastAPI
|
||||||
|
|
||||||
// 发送环信消息
|
// 发送环信消息
|
||||||
export const sendMessageApi = (data) => request({
|
export const sendMessageApi = (data) => request({
|
||||||
url: '/api/huanxin/send',
|
url: '/api/huanxin/send',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
})
|
}, 2) // 使用 FastAPI
|
||||||
|
|
|
||||||
|
|
@ -355,13 +355,19 @@ export const DanceGenerate = (data) => request({
|
||||||
url: '/dance/generate',
|
url: '/dance/generate',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
},2)//跳舞
|
},2,false)//跳舞
|
||||||
|
|
||||||
export const DanceGenerateTask = (id) => request({
|
export const DanceGenerateTask = (id) => request({
|
||||||
url: `/dance/generate/${id}`,
|
url: `/dance/generate/${id}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
},2,false)//监听生成视频结果
|
},2,false)//监听生成视频结果
|
||||||
|
|
||||||
|
export const DanceCurrent = (data) => request({
|
||||||
|
url: '/dance/current',
|
||||||
|
method: 'get',
|
||||||
|
data: data
|
||||||
|
},2,false)//获取当前进行中的跳舞任务
|
||||||
|
|
||||||
export const SingSongs = (data) => request({
|
export const SingSongs = (data) => request({
|
||||||
url: '/sing/songs',
|
url: '/sing/songs',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
|
@ -379,6 +385,12 @@ export const SingGenerateTask = (id) => request({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
},2,false)//监听生成视频结果
|
},2,false)//监听生成视频结果
|
||||||
|
|
||||||
|
export const SingCurrent = (data) => request({
|
||||||
|
url: '/sing/current',
|
||||||
|
method: 'get',
|
||||||
|
data: data
|
||||||
|
},2,false)//获取当前进行中的唱歌任务
|
||||||
|
|
||||||
export const DynamicShare = (data) => request({
|
export const DynamicShare = (data) => request({
|
||||||
url: '/dynamic/share',
|
url: '/dynamic/share',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Windows 本地开发 - 混合架构
|
// Windows 本地开发 - 混合架构
|
||||||
export const baseURL = 'http://192.168.1.164:30100' // PHP 处理界面和部分 API
|
export const baseURL = 'http://192.168.1.164:30100' // PHP 处理用户管理和界面
|
||||||
export const baseURLPy = 'http://192.168.1.164:30101' // FastAPI 处理核心 API
|
export const baseURLPy = 'http://192.168.1.164:30101' // FastAPI 处理 AI 功能
|
||||||
|
|
||||||
// 远程服务器 - 需要时取消注释
|
// 远程服务器 - 需要时取消注释
|
||||||
// export const baseURL = 'http://1.15.149.240:30100'
|
// export const baseURL = 'http://1.15.149.240:30100'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
app_debug = true
|
app_debug = false
|
||||||
app_trace = true
|
app_trace = false
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
type = mysql
|
type = mysql
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user