fix(lipsync, P0): Worker运行时ModuleNotFoundError导致任务卡在tts_processing #1854
@@ -211,12 +211,18 @@ class LipsyncService:
|
||||
normalize_emotion(emotion),
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Celery 任务提交失败,TTS 任务已创建但未触发执行: %s",
|
||||
except Exception as exc:
|
||||
# 投递失败时立即把 job 标成 failed 并写入 error_message,
|
||||
# 前端轮询时能直接看到失败原因,不会无限卡在 tts_processing。
|
||||
logger.exception(
|
||||
"Celery 任务提交失败,TTS 任务已创建但未触发执行: job_id=%s err=%s",
|
||||
job_id,
|
||||
exc_info=True,
|
||||
exc,
|
||||
)
|
||||
job.status = "failed"
|
||||
job.error_message = f"Celery 任务投递失败: {exc}"
|
||||
job.error_code = "AsyncDispatchFailed"
|
||||
job.updated_at = datetime.now(timezone.utc)
|
||||
else:
|
||||
# 2b. 直接音频模式:同步签名并提交 MediaKit
|
||||
video_url = self._sign_media_url(video_url)
|
||||
|
||||
@@ -76,11 +76,21 @@ def tts_synthesize_and_submit(
|
||||
from app.services.mediakit_client import MediaKitError, get_mediakit_client
|
||||
from sqlalchemy.orm import Session as DBSession
|
||||
|
||||
from packages.adapters.sqlalchemy_impl.database import SessionLocal
|
||||
from packages.adapters.sqlalchemy_impl.models import LipsyncJobModel
|
||||
from packages.application.cosyvoice_service import CosyVoiceError, CosyVoiceService
|
||||
from packages.shared.url_security import safe_download_bytes
|
||||
|
||||
# SessionLocal 获取:
|
||||
# - API 容器:app.db.SessionLocal(环境变量完整,导入即建引擎)
|
||||
# - Worker 容器:worker_app.db.SessionLocal(Worker 自己的 settings 初始化引擎)
|
||||
# API 侧没有 worker_app 模块 → ImportError 直接回退;
|
||||
# Worker 侧 app.db 会因缺少 API 专有环境变量抛 pydantic ValidationError,
|
||||
# 此时也要回退到 worker_app.db。
|
||||
try:
|
||||
from worker_app.db import SessionLocal # type: ignore
|
||||
except Exception: # noqa: BLE001
|
||||
from app.db import SessionLocal # type: ignore
|
||||
|
||||
db: DBSession = SessionLocal()
|
||||
try:
|
||||
job = (
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# xiaoxia-saas shared packages namespace
|
||||
@@ -0,0 +1 @@
|
||||
# adapter implementations namespace
|
||||
@@ -143,8 +143,8 @@ class TestCreateJobAsyncTTS:
|
||||
assert args[2] == "v-1" # voice_id
|
||||
assert args[3] == "测试文本" # script_text
|
||||
|
||||
def test_tts_mode_celery_dispatch_failure_still_creates_job(self):
|
||||
"""Celery dispatch 失败时,job 记录已创建,状态保持 tts_processing."""
|
||||
def test_tts_mode_celery_dispatch_failure_marks_job_failed(self):
|
||||
"""Celery dispatch 失败时,job 标为 failed 并写入 error_message,前端轮询能直接看到错误."""
|
||||
svc, client, cosy = _make_service_with_mocks()
|
||||
|
||||
with patch("app.services.lipsync_service.tts_synthesize_and_submit") as mock_task:
|
||||
@@ -157,9 +157,11 @@ class TestCreateJobAsyncTTS:
|
||||
script_text="测试文本",
|
||||
)
|
||||
|
||||
# job 已创建
|
||||
# job 已创建且状态标为 failed
|
||||
assert job is not None
|
||||
assert job.status == "tts_processing"
|
||||
assert job.status == "failed"
|
||||
assert "Celery 任务投递失败" in job.error_message
|
||||
assert job.error_code == "AsyncDispatchFailed"
|
||||
# MediaKit 未被调用
|
||||
client.submit_lipsync.assert_not_called()
|
||||
|
||||
|
||||
@@ -73,12 +73,16 @@ def _apply_all_patches(
|
||||
lipsync_tts.run() 在函数体内部懒 import 多个模块,通过 sys.modules 注入
|
||||
伪造包路径避免真实导入;对存在的模块用 patch() 替换返回值/side_effect。
|
||||
"""
|
||||
fake_db_mod = ModuleType("packages.adapters.sqlalchemy_impl.database")
|
||||
# SessionLocal 通过懒探测获取(Worker 用 worker_app.db,API 用 app.db),
|
||||
# 测试环境里两个模块都能被真实导入,必须同时 mock 保证用的是 fake session。
|
||||
fake_app_db = ModuleType("app.db")
|
||||
fake_worker_db = ModuleType("worker_app.db")
|
||||
session, factory = _build_session(job)
|
||||
fake_db_mod.SessionLocal = factory
|
||||
fake_app_db.SessionLocal = factory
|
||||
fake_worker_db.SessionLocal = factory
|
||||
|
||||
patches = [
|
||||
patch.dict(sys.modules, {"packages.adapters.sqlalchemy_impl.database": fake_db_mod}),
|
||||
patch.dict(sys.modules, {"app.db": fake_app_db, "worker_app.db": fake_worker_db}),
|
||||
patch(
|
||||
"app.tasks.lipsync_tts._sign_media_url",
|
||||
side_effect=lambda url: url + "?signed" if url else url,
|
||||
@@ -289,9 +293,11 @@ class TestTtsSynthesizeAndSubmit:
|
||||
from app.tasks.lipsync_tts import tts_synthesize_and_submit
|
||||
|
||||
job = _make_fake_job()
|
||||
fake_db_mod = ModuleType("packages.adapters.sqlalchemy_impl.database")
|
||||
fake_app_db = ModuleType("app.db")
|
||||
fake_worker_db = ModuleType("worker_app.db")
|
||||
session, factory = _build_session(job)
|
||||
fake_db_mod.SessionLocal = factory
|
||||
fake_app_db.SessionLocal = factory
|
||||
fake_worker_db.SessionLocal = factory
|
||||
|
||||
# CosyVoiceService 在 __init__ 抛 RuntimeError(非 CosyVoiceError/ValueError)
|
||||
fake_cosy_mod = ModuleType("packages.application.cosyvoice_service")
|
||||
@@ -309,7 +315,8 @@ class TestTtsSynthesizeAndSubmit:
|
||||
with patch.dict(
|
||||
sys.modules,
|
||||
{
|
||||
"packages.adapters.sqlalchemy_impl.database": fake_db_mod,
|
||||
"app.db": fake_app_db,
|
||||
"worker_app.db": fake_worker_db,
|
||||
"packages.application.cosyvoice_service": fake_cosy_mod,
|
||||
},
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user