fix(#549): 预设配音无声 - 顶层voice_id+custom_text与tts配置路径不匹配
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 / 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 Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 21s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 21s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 24s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 38s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 2m3s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m34s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m34s
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 / 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 Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 21s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 21s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 24s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 38s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 2m3s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m34s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m34s
根因:前端一键生成页面传 config.voice_id + config.custom_text(顶层字段),
统一渲染引擎从 config.tts 嵌套对象读取TTS配置,路径完全不匹配,
导致 TTS 配音从未被触发,选了预设配音也等于没选。
修复:在 _maybe_add_voiceover_layer 增加桥接兼容逻辑——
当 tts.enabled 为 False 但顶层有 voice_id + custom_text 时,
自动映射为 tts 配置并触发配音生成。
新增4个单元测试覆盖:桥接触发、tts配置优先、缺文本不触发、无配置不触发。
This commit is contained in:
@@ -675,6 +675,27 @@ class UnifiedRenderService:
|
||||
config = self.plan.config or {}
|
||||
tts_cfg = config.get("tts", {}) or {}
|
||||
|
||||
# 兼容前端顶层字段:voice_id / custom_text / voice_clone_profile_id
|
||||
# 前端一键生成页面传 config.voice_id + config.custom_text,
|
||||
# 统一渲染引擎从 config.tts 读,这里做桥接映射。
|
||||
if not tts_cfg.get("enabled"):
|
||||
top_voice_id = config.get("voice_id", "") or ""
|
||||
top_text = config.get("custom_text", "") or ""
|
||||
if top_voice_id and top_text:
|
||||
tts_cfg = {
|
||||
"enabled": True,
|
||||
"voice_id": top_voice_id,
|
||||
"text": top_text,
|
||||
"align_mode": "full",
|
||||
"overlap_mode": "replace",
|
||||
}
|
||||
logger.info(
|
||||
"[unified-render] 检测到顶层 voice_id+custom_text,桥接到 tts 配置: plan_id=%s voice_id=%s text_len=%d",
|
||||
self.plan.id,
|
||||
top_voice_id,
|
||||
len(top_text),
|
||||
)
|
||||
|
||||
tts_config = TtsConfig.parse(tts_cfg)
|
||||
if not tts_config.enabled:
|
||||
return False
|
||||
|
||||
@@ -2262,3 +2262,137 @@ class TestConcatNormalizeFourItemsComplete:
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "aac" in cmd
|
||||
assert "concat=n=3:v=0:a=1" in " ".join(cmd)
|
||||
|
||||
|
||||
class TestVoiceoverTopLevelConfigBridge:
|
||||
"""顶层 voice_id + custom_text 桥接到 tts 配置的兼容性测试.
|
||||
|
||||
前端一键生成页面传 config.voice_id + config.custom_text(顶层字段),
|
||||
统一渲染引擎从 config.tts 读取。桥接逻辑确保两条路径都能工作。
|
||||
"""
|
||||
|
||||
def test_top_level_voice_id_with_text_triggers_tts(self):
|
||||
"""顶层 voice_id + custom_text 能触发 TTS 配音(桥接生效)。"""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
clips = [_make_clip("c1", "main", order=0, duration=5.0)]
|
||||
asset_paths = {"asset_c1.mp4": Path("/tmp/asset_c1.mp4")}
|
||||
plan = FakePlan(
|
||||
id="plan_tts_001",
|
||||
config={
|
||||
"voice_id": "longxiaoxia_v3",
|
||||
"custom_text": "大家好,欢迎来到我的频道",
|
||||
},
|
||||
)
|
||||
svc = UnifiedRenderService(
|
||||
plan=plan,
|
||||
clips=clips,
|
||||
asset_path_map=asset_paths,
|
||||
work_dir=Path("/tmp/test_tts"),
|
||||
)
|
||||
|
||||
mock_seg = MagicMock()
|
||||
mock_seg.audio_path = Path("/tmp/test_tts/tts/voiceover_full.wav")
|
||||
mock_seg.start_time = 0.0
|
||||
mock_seg.duration = 3.0
|
||||
mock_result = MagicMock()
|
||||
mock_result.success = True
|
||||
mock_result.segments = [mock_seg]
|
||||
mock_result.total_duration = 3.0
|
||||
|
||||
with (
|
||||
_patch_path_exists(),
|
||||
patch("video_processing.unified_render_service.probe_duration", return_value=5.0),
|
||||
patch(
|
||||
"video_processing.tts_engine.TtsEngine.generate_full_voiceover",
|
||||
return_value=mock_result,
|
||||
),
|
||||
):
|
||||
resolved = svc._resolve_clips()
|
||||
layers = svc._group_clips_into_layers(resolved)
|
||||
result = svc._maybe_add_voiceover_layer(layers, video_duration=5.0)
|
||||
|
||||
assert result is True, "顶层 voice_id + custom_text 应触发 TTS 配音"
|
||||
# 应有 audio 图层
|
||||
audio_layer = next((l for l in layers if l.role == "audio"), None)
|
||||
assert audio_layer is not None, "应添加 audio 图层"
|
||||
assert len(audio_layer.clips) == 1, "应有 1 个配音片段"
|
||||
|
||||
def test_tts_config_takes_priority(self):
|
||||
"""config.tts.enabled 已配置时,以 tts 配置为准,不触发桥接。"""
|
||||
clips = [_make_clip("c1", "main", order=0, duration=5.0)]
|
||||
asset_paths = {"asset_c1.mp4": Path("/tmp/asset_c1.mp4")}
|
||||
# tts.enabled=True 但 text 为空(应失败),顶层有 text
|
||||
plan = FakePlan(
|
||||
id="plan_tts_002",
|
||||
config={
|
||||
"voice_id": "longxiaoxia_v3",
|
||||
"custom_text": "顶层文本不生效",
|
||||
"tts": {
|
||||
"enabled": True,
|
||||
"voice_id": "longxiaochun_v3",
|
||||
"text": "", # tts 配置里 text 为空
|
||||
},
|
||||
},
|
||||
)
|
||||
svc = UnifiedRenderService(
|
||||
plan=plan,
|
||||
clips=clips,
|
||||
asset_path_map=asset_paths,
|
||||
work_dir=Path("/tmp/test_tts"),
|
||||
)
|
||||
|
||||
with (
|
||||
_patch_path_exists(),
|
||||
patch("video_processing.unified_render_service.probe_duration", return_value=5.0),
|
||||
):
|
||||
resolved = svc._resolve_clips()
|
||||
layers = svc._group_clips_into_layers(resolved)
|
||||
result = svc._maybe_add_voiceover_layer(layers, video_duration=5.0)
|
||||
|
||||
# tts.enabled=True 但 text 为空 → 生成失败 → 返回 False
|
||||
# 关键是不触发桥接(不会用顶层的 custom_text)
|
||||
assert result is False
|
||||
|
||||
def test_top_level_voice_id_without_text_no_trigger(self):
|
||||
"""只有 voice_id 没有 custom_text 不触发 TTS 配音。"""
|
||||
clips = [_make_clip("c1", "main", order=0, duration=5.0)]
|
||||
asset_paths = {"asset_c1.mp4": Path("/tmp/asset_c1.mp4")}
|
||||
plan = FakePlan(
|
||||
id="plan_tts_003",
|
||||
config={"voice_id": "longxiaoxia_v3", "custom_text": ""},
|
||||
)
|
||||
svc = UnifiedRenderService(
|
||||
plan=plan,
|
||||
clips=clips,
|
||||
asset_path_map=asset_paths,
|
||||
work_dir=Path("/tmp/test_tts"),
|
||||
)
|
||||
|
||||
with (
|
||||
_patch_path_exists(),
|
||||
patch("video_processing.unified_render_service.probe_duration", return_value=5.0),
|
||||
):
|
||||
resolved = svc._resolve_clips()
|
||||
layers = svc._group_clips_into_layers(resolved)
|
||||
result = svc._maybe_add_voiceover_layer(layers, video_duration=5.0)
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_no_voice_config_no_trigger(self):
|
||||
"""没有 voice_id 也没有 tts 配置时,不触发配音。"""
|
||||
clips = [_make_clip("c1", "main", order=0, duration=5.0)]
|
||||
asset_paths = {"asset_c1.mp4": Path("/tmp/asset_c1.mp4")}
|
||||
svc = _make_service(clips, asset_paths)
|
||||
|
||||
with (
|
||||
_patch_path_exists(),
|
||||
patch("video_processing.unified_render_service.probe_duration", return_value=5.0),
|
||||
):
|
||||
resolved = svc._resolve_clips()
|
||||
layers = svc._group_clips_into_layers(resolved)
|
||||
result = svc._maybe_add_voiceover_layer(layers, video_duration=5.0)
|
||||
|
||||
assert result is False
|
||||
# 没有 audio 图层
|
||||
assert not any(l.role == "audio" for l in layers)
|
||||
Reference in New Issue
Block a user