Compare commits

...

2 Commits

Author SHA1 Message Date
CI Bot cedb9b4837 style: auto-format with black + isort + ruff + prettier [skip ci-format-check]
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 3s
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 / Check if frontend-only change (pull_request) Successful in 10s
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 / PR Build API Image (pull_request) Successful in 2m22s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m16s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m40s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m42s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m11s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m49s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m38s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m31s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 7m16s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 7m36s
AI Code Review / AI Code Review (pull_request) Successful in 7m48s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m48s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 17m20s
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 / Validate - Python (mypy + alembic) (pull_request) Successful in 5m46s
CI/CD Pipeline / Build Production API 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 3s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 35s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 50s
2026-10-15 09:32:12 +00:00
Agent 0f1d027c73 fix(lipsync): #2076 预合成模式补齐 script_text,修复 Ditto 任务 script_len=0
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m50s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 48s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 16s
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 / PR Build Web Image (pull_request) Successful in 1m33s
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 1m50s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m19s
AI Code Review / AI Code Review (pull_request) Successful in 6m49s
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
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 - Style (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
根因:前端 #1875(053b0063) 起预合成 payload 不带 script_text,
job.script_text 落库为空 → Ditto 任务 script_len=0 → 情绪分析被跳过。

修复(双保险):
- 前端 AiAvatarPage 预合成 payload 补传 script_text: state.scriptText
- 后端 create_job 预合成模式 script_text 为空时从 sentence_timings 拼接兜底
  (覆盖所有调用方;timings 每项自带 text 字段)

与 PR#2292 无关(2292 只动 build_timeline/intensity 内部逻辑)。

测试:新增 3 用例(空时拼接兜底/显式传入保留原文/空 timings 退化直接音频),
test_lipsync_routes.py 45 passed
2026-10-15 17:19:45 +08:00
3 changed files with 89 additions and 0 deletions
+6
View File
@@ -623,6 +623,12 @@ class LipsyncService:
# 预合成模式:script_text 可空(因为 timings 已自带句子文本),但仍建议传
if not isinstance(sentence_timings, list) or len(sentence_timings) == 0:
raise MediaKitError("预合成模式 sentence_timings 不能为空", code="InvalidInput")
# 文案为空时从 sentence_timings 拼接兜底(Ditto 情绪分析依赖 script_text;
# 前端 #1875 起预合成 payload 不带 script_text,导致 job.script_text 落库为空)
if not (script_text or "").strip():
script_text = "".join(
str(item.get("text") or "") for item in sentence_timings if isinstance(item, dict)
).strip()
# 0.5 lip_sync 计费预扣(全局 points 开关 + 功能开关均开才扣)
prepaid_credits = 0.0
@@ -255,6 +255,8 @@ const AiAvatarPage: React.FC = () => {
audio_url: state.ttsPreview.audioUrl!,
audio_duration: state.ttsPreview.duration,
sentence_timings: state.ttsPreview.sentenceTimings,
// 预合成也必须带文案:后端落库 job.script_text,Ditto 情绪分析依赖它
script_text: state.scriptText,
enable_video_loop: true,
}
} else {
+81
View File
@@ -486,6 +486,87 @@ class TestLipsyncServiceUnit:
assert call_kwargs.kwargs["audio_url"] == "https://example.com/direct-audio.mp3"
assert job.audio_url == "https://example.com/direct-audio.mp3"
def test_create_job_presynth_backfills_script_text(self, mock_mediakit, mock_cosyvoice):
"""预合成模式 script_text 为空时,从 sentence_timings 拼接兜底.
背景:前端 #1875 起预合成 payload 不带 script_text,导致 job.script_text
落库为空,Ditto 任务 script_len=0、情绪分析被跳过。
"""
from app.services.lipsync_service import LipsyncService
mock_db = MagicMock()
svc = LipsyncService(
mock_db,
client=mock_mediakit,
cosyvoice_service=mock_cosyvoice,
voice_clone_repo=MagicMock(),
)
svc.settings.use_gpu_lipsync = False
timings = [
{"index": 0, "text": "大家好,", "start_time": 0.0, "end_time": 1.0},
{"index": 1, "text": "欢迎来到直播间。", "start_time": 1.0, "end_time": 2.5},
]
job = svc.create_job(
user_id="user-1",
video_url="https://example.com/video.mp4",
audio_url="https://example.com/audio.mp3",
audio_duration=2.5,
sentence_timings=timings,
script_text="", # 前端预合成 payload 不带文案
)
assert job.script_text == "大家好,欢迎来到直播间。"
def test_create_job_presynth_keeps_supplied_script_text(self, mock_mediakit, mock_cosyvoice):
"""预合成模式显式传了 script_text 时,保留原文不做拼接."""
from app.services.lipsync_service import LipsyncService
mock_db = MagicMock()
svc = LipsyncService(
mock_db,
client=mock_mediakit,
cosyvoice_service=mock_cosyvoice,
voice_clone_repo=MagicMock(),
)
svc.settings.use_gpu_lipsync = False
timings = [
{"index": 0, "text": "分句一", "start_time": 0.0, "end_time": 1.0},
]
job = svc.create_job(
user_id="user-1",
video_url="https://example.com/video.mp4",
audio_url="https://example.com/audio.mp3",
audio_duration=1.0,
sentence_timings=timings,
script_text="原始文案,含标点。",
)
assert job.script_text == "原始文案,含标点。"
def test_create_job_presynth_empty_timings_falls_back_to_direct(self, mock_mediakit, mock_cosyvoice):
"""sentence_timings 空列表时 is_pre_synth=False,退化为直接音频模式(语义固化)."""
from app.services.lipsync_service import LipsyncService
mock_db = MagicMock()
svc = LipsyncService(
mock_db,
client=mock_mediakit,
cosyvoice_service=mock_cosyvoice,
voice_clone_repo=MagicMock(),
)
svc.settings.use_gpu_lipsync = False
job = svc.create_job(
user_id="user-1",
video_url="https://example.com/video.mp4",
audio_url="https://example.com/audio.mp3",
sentence_timings=[],
script_text="",
)
# 直接音频模式:无 timings 可拼,script_text 保持空(不报错)
assert job.script_text == ""
mock_mediakit.submit_lipsync.assert_called_once()
class TestErrorHandling:
"""v3: 音色解析与错误码在 service 层处理,路由层做 HTTP 状态码映射."""