Files
xiaoxia-saas/apps/api/app/tasks/lipsync_gpu.py
T
xiaoxia 08de0d9946
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m40s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m10s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m16s
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
perf: async GPU lipsync inference + fix 16x performance regression
- Move GPU wait_for_result to Celery background task (lipsync_gpu_process_async)
  POST /lipsync/jobs now returns <1s instead of blocking 200s+
- Rewrite musetalk_server.py: MuseTalk receives full audio directly
  (v2 architecture) — no pre-looping video before inference
  Output video length = audio length, mux is fast stream copy
- Frontend polls GET /lipsync/jobs/{id} for status updates
- refresh_job_status: GPU async path (processing + no mediakit_task_id)
  skips MediaKit polling; stale jobs (>30min) auto-marked failed
- 21 unit tests pass (11 GPU integration + 10 musetalk audio mux)

Co-Authored-By: Coze <coze-opensource@bytedance.com>
2026-09-20 09:43:47 +08:00

191 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""GPU MuseTalk 异步推理任务 — 将 GPU 推理等待从 HTTP 请求移至 Celery 后台执行.
优化目标:将 POST /lipsync/jobs 的 API 响应时间从 >200s 降到 <1s。
任务流程:
1. 加载 LipsyncJob,获取 gpu_task_id
2. 调用 GpuLipsyncService.wait_for_result 轮询等待 GPU 完成
3. 签名结果 URL(7 天),更新 job 为 completed
4. 失败/超时时:尝试 MediaKit 兜底,若仍失败则标记 job 为 failed
使用 @shared_task 确保被 Worker 侧 celery_app 正确注册。
"""
import logging
from datetime import UTC, datetime
from celery import shared_task
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.models import LipsyncJobModel
from packages.shared.storage import get_shared_storage_service
logger = logging.getLogger(__name__)
# 与 LipsyncService 保持一致
_MEDIAKIT_URL_TTL_SECONDS = 7 * 24 * 3600
def _get_db_session() -> Session:
"""获取 DB session(兼容 API 和 Worker 两种运行时)."""
try:
from worker_app.db import SessionLocal # type: ignore
except ImportError:
from app.db import SessionLocal # type: ignore
return SessionLocal()
def _sign_media_url(url: str) -> str:
"""对自家 OSS URL 签 7 天预签名。"""
if not url:
return url
try:
from urllib.parse import urlparse
storage = get_shared_storage_service()
public_base = getattr(storage, "public_url", "")
if not isinstance(public_base, str) or not public_base:
return url
own_host = urlparse(public_base).netloc.lower()
host = urlparse(url).netloc.lower()
if not own_host or host != own_host:
return url
return storage.get_download_url(url, expires_seconds=_MEDIAKIT_URL_TTL_SECONDS)
except Exception:
return url
@shared_task(
name="lipsync_gpu_process_async",
bind=True,
max_retries=0,
acks_late=True,
)
def lipsync_gpu_process_async(self, job_id: str, user_id: str, gpu_task_id: str) -> None:
"""异步处理 GPU MuseTalk 推理。
Args:
job_id: LipsyncJob 的 ID
user_id: 用户 ID
gpu_task_id: GpuLipsyncTask 的 ID
"""
db: Session = _get_db_session()
try:
job = db.query(LipsyncJobModel).filter_by(id=job_id, user_id=user_id).first()
if job is None:
logger.error("[lipsync_gpu_async] job 不存在: job_id=%s", job_id)
return
# 确保状态为 processing
if job.status not in ("processing", "gpu_processing"):
logger.warning(
"[lipsync_gpu_async] job 状态异常,跳过: job_id=%s status=%s",
job_id,
job.status,
)
return
from app.services.gpu_lipsync_service import GpuLipsyncService
gpu_svc = GpuLipsyncService(db)
final_task = gpu_svc.wait_for_result(gpu_task_id)
if final_task is None:
logger.warning(
"[lipsync_gpu_async] GPU 超时,回退 MediaKit: job_id=%s gpu_task=%s",
job_id,
gpu_task_id,
)
_fallback_to_mediakit(db, job)
return
if final_task.status != "done":
logger.warning(
"[lipsync_gpu_async] GPU 失败,回退 MediaKit: job_id=%s gpu_task=%s status=%s",
job_id,
gpu_task_id,
final_task.status,
)
_fallback_to_mediakit(db, job)
return
# 签名结果 URL
result_url = final_task.result_url or ""
try:
storage = get_shared_storage_service()
signed = storage.get_download_url(result_url, expires_seconds=_MEDIAKIT_URL_TTL_SECONDS)
if signed:
result_url = signed
except Exception as exc:
logger.warning(
"[lipsync_gpu_async] 签名失败,用原 URL: job_id=%s err=%s",
job_id,
exc,
)
job.status = "completed"
job.output_video_url = result_url
job.output_duration = final_task.result_duration or 0.0
job.completed_at = datetime.now(UTC)
job.updated_at = datetime.now(UTC)
db.commit()
logger.info(
"[lipsync_gpu_async] GPU 完成: job_id=%s duration=%.2f",
job_id,
job.output_duration,
)
except Exception as exc:
logger.exception("[lipsync_gpu_async] 异常: job_id=%s err=%s", job_id, exc)
try:
job = db.query(LipsyncJobModel).filter_by(id=job_id).first()
if job:
job.status = "failed"
job.error_message = f"GPU 异步处理异常: {exc}"
job.error_code = "GpuAsyncError"
job.updated_at = datetime.now(UTC)
db.commit()
except Exception:
pass
finally:
db.close()
def _fallback_to_mediakit(db: Session, job: LipsyncJobModel) -> None:
"""GPU 失败时回退到 MediaKit 云端渲染。"""
try:
from app.services.mediakit_client import MediaKitError, get_mediakit_client
client = get_mediakit_client()
video_url = _sign_media_url(job.video_url)
audio_url = _sign_media_url(job.audio_url)
result = client.submit_lipsync(
video_url=video_url,
audio_url=audio_url,
enable_video_loop=job.enable_video_loop,
client_token=job.id,
)
job.mediakit_task_id = result["task_id"]
job.status = "submitted"
job.submitted_at = datetime.now(UTC)
job.updated_at = datetime.now(UTC)
db.commit()
logger.info(
"[lipsync_gpu_async] 已回退 MediaKit: job_id=%s task_id=%s",
job.id,
result["task_id"],
)
except MediaKitError as exc:
job.status = "failed"
job.error_message = str(exc)
job.error_code = exc.code
job.updated_at = datetime.now(UTC)
db.commit()
logger.error("[lipsync_gpu_async] MediaKit 也失败: job_id=%s err=%s", job.id, exc)
except Exception as exc:
job.status = "failed"
job.error_message = f"GPU+MediaKit 均失败: {exc}"
job.error_code = "FallbackFailed"
job.updated_at = datetime.now(UTC)
db.commit()
logger.error("[lipsync_gpu_async] 兜底异常: job_id=%s err=%s", job.id, exc)