from fastapi import APIRouter, HTTPException, BackgroundTasks from pydantic import BaseModel from typing import Optional from pathlib import Path import uuid import traceback import time from app.services.tts_service import TTSService from app.services.video_service import VideoService from app.services.lipsync_service import LipSyncService from app.core.config import settings router = APIRouter() class GenerateRequest(BaseModel): text: str voice: str = "zh-CN-YunxiNeural" material_path: str tasks = {} # In-memory task store # 缓存 LipSync 服务实例和健康状态 _lipsync_service: Optional[LipSyncService] = None _lipsync_ready: Optional[bool] = None _lipsync_last_check: float = 0 def _get_lipsync_service() -> LipSyncService: """获取或创建 LipSync 服务实例(单例模式,避免重复初始化)""" global _lipsync_service if _lipsync_service is None: _lipsync_service = LipSyncService() return _lipsync_service async def _check_lipsync_ready(force: bool = False) -> bool: """检查 LipSync 是否就绪(带缓存,5分钟内不重复检查)""" global _lipsync_ready, _lipsync_last_check now = time.time() # 5分钟缓存 if not force and _lipsync_ready is not None and (now - _lipsync_last_check) < 300: return _lipsync_ready lipsync = _get_lipsync_service() health = await lipsync.check_health() _lipsync_ready = health.get("ready", False) _lipsync_last_check = now print(f"[LipSync] Health check: ready={_lipsync_ready}") return _lipsync_ready async def _process_video_generation(task_id: str, req: GenerateRequest): try: start_time = time.time() # Resolve path if it's relative input_material_path = Path(req.material_path) if not input_material_path.is_absolute(): input_material_path = settings.BASE_DIR.parent / req.material_path tasks[task_id]["status"] = "processing" tasks[task_id]["progress"] = 5 tasks[task_id]["message"] = "正在初始化..." # 1. TTS - 进度 5% -> 25% tasks[task_id]["message"] = "正在生成语音 (TTS)..." tasks[task_id]["progress"] = 10 tts = TTSService() audio_path = settings.OUTPUT_DIR / f"{task_id}_audio.mp3" await tts.generate_audio(req.text, req.voice, str(audio_path)) tts_time = time.time() - start_time print(f"[Pipeline] TTS completed in {tts_time:.1f}s") tasks[task_id]["progress"] = 25 # 2. LipSync - 进度 25% -> 85% tasks[task_id]["message"] = "正在合成唇形 (LatentSync)..." tasks[task_id]["progress"] = 30 lipsync = _get_lipsync_service() lipsync_video_path = settings.OUTPUT_DIR / f"{task_id}_lipsync.mp4" # 使用缓存的健康检查结果 lipsync_start = time.time() is_ready = await _check_lipsync_ready() if is_ready: print(f"[LipSync] Starting LatentSync inference...") tasks[task_id]["progress"] = 35 tasks[task_id]["message"] = "正在运行 LatentSync 推理..." await lipsync.generate(str(input_material_path), str(audio_path), str(lipsync_video_path)) else: # Skip lipsync if not available print(f"[LipSync] LatentSync not ready, copying original video") tasks[task_id]["message"] = "唇形同步不可用,使用原始视频..." import shutil shutil.copy(str(input_material_path), lipsync_video_path) lipsync_time = time.time() - lipsync_start print(f"[Pipeline] LipSync completed in {lipsync_time:.1f}s") tasks[task_id]["progress"] = 85 # 3. Composition - 进度 85% -> 100% tasks[task_id]["message"] = "正在合成最终视频..." tasks[task_id]["progress"] = 90 video = VideoService() final_output = settings.OUTPUT_DIR / f"{task_id}_output.mp4" await video.compose(str(lipsync_video_path), str(audio_path), str(final_output)) total_time = time.time() - start_time print(f"[Pipeline] Total generation time: {total_time:.1f}s") tasks[task_id]["status"] = "completed" tasks[task_id]["progress"] = 100 tasks[task_id]["message"] = f"生成完成!耗时 {total_time:.0f} 秒" tasks[task_id]["output"] = str(final_output) tasks[task_id]["download_url"] = f"/outputs/{final_output.name}" except Exception as e: tasks[task_id]["status"] = "failed" tasks[task_id]["message"] = f"错误: {str(e)}" tasks[task_id]["error"] = traceback.format_exc() @router.post("/generate") async def generate_video(req: GenerateRequest, background_tasks: BackgroundTasks): task_id = str(uuid.uuid4()) tasks[task_id] = {"status": "pending", "task_id": task_id, "progress": 0} background_tasks.add_task(_process_video_generation, task_id, req) return {"task_id": task_id} @router.get("/tasks/{task_id}") async def get_task(task_id: str): return tasks.get(task_id, {"status": "not_found"}) @router.get("/tasks") async def list_tasks(): return {"tasks": list(tasks.values())} @router.get("/lipsync/health") async def lipsync_health(): """获取 LipSync 服务健康状态""" lipsync = _get_lipsync_service() return await lipsync.check_health()