fix(1978): GPU lipsync 结果 URL 签 7 天 + 外部 TTS 音频转存自家 OSS
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
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 2s
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 / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web 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 / PR Build Worker Image (pull_request) Successful in 2m50s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m58s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m3s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m35s
AI Code Review / AI Code Review (pull_request) Successful in 6m47s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 10m55s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 11m24s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 11m35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m37s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 14m7s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 20m26s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 20s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 35s

Bug1: _submit_to_gpu 用 _sign_media_url 签 GPU 结果,但 result_url 是裸
OSS key(gpu-lipsync/results/{task}.mp4,无 host),_sign_media_url 只对
自家 host 的 URL 签名,裸 key 原样返回导致前端无法播放。改为直接调
storage.get_download_url(key, expires_seconds=MEDIAKIT_URL_TTL_SECONDS=7天)。

Bug2: 预合成 TTS 音频 audio_url 是 dashscope-result 第三方临时 OSS 地址,
RTX2060 家庭网络拉不到,3 次失败后回退 MediaKit。创建 GPU 任务前新增
_persist_external_audio_for_gpu:非自家 OSS 域名音频下载转存
lipsync-tts/{user_id}/{job_id}.mp3,用自家 URL 建任务;转存失败回退原
URL 不阻断;自家 key/URL 不重复转存。

测试:test_lipsync_gpu_integration 新增 3 用例(外部音频转存/自家不转存/
转存失败兜底),成功路径断言 7 天签名。
This commit is contained in:
2026-09-19 23:43:35 +08:00
parent 112f0eb277
commit d5a4a40f2a
2 changed files with 172 additions and 16 deletions
+77 -10
View File
@@ -250,7 +250,7 @@ class LipsyncService:
gpu_task.id,
job.output_duration,
)
# 转存到持久 OSS 路径(GPU 结果已在 gpu-lipsync/results/ 下,直接签短链)
# output_video_url 已是 _submit_to_gpu 内签好的 7 天预签名 URL
return
# wait_for_result 返回 None 表示超时/最终失败 → 继续走 MediaKit 兜底
logger.warning("[lipsync] GPU 任务等待超时或失败,回退 MediaKit: job_id=%s", job.id)
@@ -293,22 +293,77 @@ class LipsyncService:
# ── GPU MuseTalk 路径 ────────────────────────────────────────────────
def _is_own_oss_url(self, url: str, storage) -> bool:
"""判断 URL / 存储 key 是否属于自家 OSS。
- 裸存储 key(无 scheme):自家对象
- host 与 storage.public_url host 一致:自家对象
- 其余 http(s) 公网链接(如 dashscope-result 临时地址):外部对象
"""
if not url:
return False
parsed = urlparse(url)
if not parsed.scheme:
return True # 裸存储 key
public_base = getattr(storage, "public_url", "")
own_host = urlparse(public_base).netloc.lower() if public_base else ""
return bool(own_host) and parsed.netloc.lower() == own_host
def _persist_external_audio_for_gpu(self, *, job, storage) -> Optional[str]:
"""GPU 任务创建前,把外部域名的预合成 TTS 音频转存到自家 OSS。
Worker 部署在用户家庭网络,dashscope-result 等第三方临时 OSS 地址
可能无法访问;转存后 gpu_svc 在 poll 时会签自家预签名 URL 给 Worker。
已是自家 OSS 对象(含裸 key)直接返回 None(无需转存);
转存失败返回 None,调用方回退使用原始 URL(最坏情况是 Worker 拉取失败,
服务端重试耗尽后回退 MediaKit,不阻断业务)。
"""
if self._is_own_oss_url(job.audio_url, storage):
return None
try:
audio_data = safe_download_bytes(
job.audio_url,
purpose="lipsync_gpu_tts_audio",
allowed_mime_types=ALLOWED_AUDIO_MIME_TYPES,
timeout=60.0,
)
storage_key = f"lipsync-tts/{job.user_id}/{job.id}.mp3"
permanent_url = storage.upload_file(io.BytesIO(audio_data), storage_key, content_type="audio/mpeg")
logger.info(
"[lipsync] GPU 任务外部音频已转存自家 OSS: job_id=%s key=%s",
job.id,
storage_key,
)
return permanent_url
except Exception as exc:
logger.warning(
"[lipsync] GPU 任务外部音频转存 OSS 失败,回退原始 URL: job_id=%s err=%s",
job.id,
exc,
)
return None
def _submit_to_gpu(self, *, job, gpu_svc) -> Optional[object]:
"""创建 GPU 任务并同步等待结果。
成功返回终态 task 对象(status=done);超时或 GPU 最终失败返回 None,
调用方回退 MediaKit。
注意:job.video_url / job.audio_url 可能是:
- 自家 OSS 存储 key(storage.is_own_url 判断,gpu_svc.create_task 内部
get_download_url 会自动签预签名 URL 给 Worker)
- 外部公网 URL(CosyVoice 临时链接等):poll 返回时原样透传给 Worker,
Worker 可直接 GET 下载。
输入处理:
- job.video_url 为用户上传视频,已在自家 OSS(裸 key 或自家 URL),
gpu_svc 在 poll 时签预签名 URL 给 Worker。
- job.audio_url 可能是预合成 TTS 的第三方临时地址(如
dashscope-result-bj.oss-cn-beijing.aliyuncs.com),Worker 家庭网络
拉不到;创建任务前先转存自家 OSS 再传入。
"""
storage = get_shared_storage_service()
# 外部音频(dashscope 临时链接等)先转存自家 OSS,避免 Worker 家庭网络拉取失败
persisted_audio_url = self._persist_external_audio_for_gpu(job=job, storage=storage)
audio_url_for_task = persisted_audio_url or job.audio_url
# 创建 GPU 任务
gpu_task = gpu_svc.create_task(
video_url=job.video_url,
audio_url=job.audio_url,
audio_url=audio_url_for_task,
lipsync_job_id=job.id,
user_id=job.user_id,
project_id=job.project_id,
@@ -331,9 +386,21 @@ class LipsyncService:
final_task.error_msg,
)
return None
# result_url 是 OSS 存储 key;签一个长有效期 URL 写回 job.output_video_url
result_signed = self._sign_media_url(final_task.result_url)
final_task.result_url = result_signed or final_task.result_url
# result_url 是 OSS 存储 key(gpu-lipsync/results/{task_id}.mp4,无 host,
# _sign_media_url 对裸 key 不会签名);直接用 storage 签 7 天预签名 URL
# 写回 job.output_video_url,保证前端拿到可直接下载播放的地址
try:
signed_result_url = storage.get_download_url(
final_task.result_url, expires_seconds=MEDIAKIT_URL_TTL_SECONDS
)
if signed_result_url:
final_task.result_url = signed_result_url
except Exception as exc:
logger.warning(
"[lipsync] GPU 结果视频签名失败,回退原始 result_url: gpu_task=%s err=%s",
gpu_task.id,
exc,
)
return final_task
# ── 创建任务 ──────────────────────────────────────────────────────────