From 5b76f500f2167a42892d4f7e5553d47ed56ad7a0 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 25 Aug 2026 22:25:36 +0800 Subject: [PATCH 1/2] fix: title slash newline + TTS/voice-library replace mode mutes original audio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. escape_ass_text: convert / and / to ASS hard newline \\N before automatic wrapping, so user manual line breaks take priority. 2. _maybe_add_voiceover_layer: when overlap_mode=='replace', set volume=0 on all main/broll layer clips after adding TTS audio. 3. _maybe_add_voice_library_layer: always mute main/broll clips when voice library audio is added (voice library replaces original audio). 4. Add 7 new unit tests covering slash conversion and integration. --- .../unified_render_service.py | 21 ++++++++++ packages/domain/ass_subtitle_builder.py | 2 + .../unit/domain/test_ass_subtitle_builder.py | 8 ++++ tests/unit/test_ass_subtitle_builder.py | 13 +++++++ tests/unit/test_title_render_consistency.py | 38 +++++++++++++++++++ 5 files changed, 82 insertions(+) diff --git a/apps/worker/video_processing/unified_render_service.py b/apps/worker/video_processing/unified_render_service.py index 9667ce5c8..3906869b0 100755 --- a/apps/worker/video_processing/unified_render_service.py +++ b/apps/worker/video_processing/unified_render_service.py @@ -824,6 +824,17 @@ class UnifiedRenderService: ) audio_layer.clips.append(vo_clip) + # replace 模式:静音原视频音轨(main + broll 图层) + if tts_config.overlap_mode == "replace": + for layer in layers: + if layer.role in ("main", "broll"): + for clip in layer.clips: + clip.config["volume"] = 0 + logger.info( + "TTS replace 模式:已静音原视频音轨: plan_id=%s", + self.plan.id, + ) + logger.info( "TTS 配音已添加: plan_id=%s voice_id=%s segments=%d total_%.2fs", self.plan.id, @@ -889,6 +900,16 @@ class UnifiedRenderService: ) audio_layer.clips.append(vo_clip) + # 配音素材库默认替换原音:静音原视频音轨(main + broll 图层) + for layer in layers: + if layer.role in ("main", "broll"): + for clip in layer.clips: + clip.config["volume"] = 0 + logger.info( + "配音素材库:已静音原视频音轨: plan_id=%s", + self.plan.id, + ) + logger.info( "配音素材库音频已添加到 audio 图层: plan_id=%s duration=%.2fs", self.plan.id, diff --git a/packages/domain/ass_subtitle_builder.py b/packages/domain/ass_subtitle_builder.py index 0da7fea4a..e80b19ff2 100755 --- a/packages/domain/ass_subtitle_builder.py +++ b/packages/domain/ass_subtitle_builder.py @@ -147,6 +147,8 @@ def escape_ass_text(text: str) -> str: Returns: 转义后的 ASS 文本 """ + # 用户手动换行符(半角/全角斜杠)转为 ASS 硬换行(在自动换行之前优先处理) + text = text.replace("/", "\\N").replace("/", "\\N") # 将实际换行转为 ASS 硬换行 text = text.replace("\r\n", "\\N").replace("\n", "\\N").replace("\r", "\\N") # 转义大括号(ASS 用它做样式覆盖标签) diff --git a/tests/unit/domain/test_ass_subtitle_builder.py b/tests/unit/domain/test_ass_subtitle_builder.py index 97adda325..1a7913983 100644 --- a/tests/unit/domain/test_ass_subtitle_builder.py +++ b/tests/unit/domain/test_ass_subtitle_builder.py @@ -218,6 +218,14 @@ class TestEscapeAssText: def test_chinese_text(self): assert escape_ass_text("你好世界") == "你好世界" + def test_slash_converted_to_newline(self): + """半角斜杠 / 应转为 ASS 硬换行。""" + assert escape_ass_text("标题一/标题二") == "标题一\\N标题二" + + def test_fullwidth_slash_converted_to_newline(self): + """全角斜杠 / 应转为 ASS 硬换行。""" + assert escape_ass_text("标题一/标题二") == "标题一\\N标题二" + def test_backslash_n_in_input(self): # 文本里本身有 \n 字符串(不是换行符) result = escape_ass_text("\\n") diff --git a/tests/unit/test_ass_subtitle_builder.py b/tests/unit/test_ass_subtitle_builder.py index 2772e7948..b2f6e2da4 100755 --- a/tests/unit/test_ass_subtitle_builder.py +++ b/tests/unit/test_ass_subtitle_builder.py @@ -190,6 +190,19 @@ class TestEscapeAssText: def test_chinese_text(self): assert escape_ass_text("你好世界") == "你好世界" + def test_slash_converted_to_newline(self): + """半角斜杠 / 应转为 ASS 硬换行。""" + assert escape_ass_text("第一行/第二行") == "第一行\\N第二行" + + def test_fullwidth_slash_converted_to_newline(self): + """全角斜杠 / 应转为 ASS 硬换行。""" + assert escape_ass_text("第一行/第二行") == "第一行\\N第二行" + + def test_mixed_slashes_and_newlines(self): + """斜杠和换行符都应转为硬换行。""" + result = escape_ass_text("A/B\nC/D") + assert result == "A\\NB\\NC\\ND" + # ── 时间格式化 ──────────────────────────────────────────────────────────────── diff --git a/tests/unit/test_title_render_consistency.py b/tests/unit/test_title_render_consistency.py index 677eabc34..76cb8fb0f 100644 --- a/tests/unit/test_title_render_consistency.py +++ b/tests/unit/test_title_render_consistency.py @@ -8,6 +8,8 @@ """ import sys + +import pytest from pathlib import Path # Ensure packages is importable @@ -239,3 +241,39 @@ class TestFullStyleConsistency: assert int(fields[17]) == 2 # Alignment = 8 (top) assert int(fields[18]) == 8 + + +class TestTitleSlashNewline: + """用户输入 / 或 / 应触发标题换行。""" + + def test_halfwidth_slash_in_title(self): + content = build_ass_content( + video_width=1080, + video_height=1920, + video_duration=10.0, + title_text="第一行/第二行", + title_config={"size": 48}, + ) + for line in content.splitlines(): + if "Dialogue" in line and "TitleStyle" in line: + assert "\\N" in line, f"斜杠应转为换行: {line}" + assert "第一行" in line + assert "第二行" in line + break + else: + pytest.fail("未找到 TitleStyle Dialogue 行") + + def test_fullwidth_slash_in_title(self): + content = build_ass_content( + video_width=1080, + video_height=1920, + video_duration=10.0, + title_text="第一行/第二行", + title_config={"size": 48}, + ) + for line in content.splitlines(): + if "Dialogue" in line and "TitleStyle" in line: + assert "\\N" in line, f"全角斜杠应转为换行: {line}" + break + else: + pytest.fail("未找到 TitleStyle Dialogue 行") -- 2.54.0 From 6aad1c25563588b39d07a993305877d92710da68 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Tue, 25 Aug 2026 14:37:55 +0000 Subject: [PATCH 2/2] style: auto-format with black + isort + prettier [skip ci-format-check] --- tests/unit/test_title_render_consistency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_title_render_consistency.py b/tests/unit/test_title_render_consistency.py index 76cb8fb0f..a2e8e9646 100644 --- a/tests/unit/test_title_render_consistency.py +++ b/tests/unit/test_title_render_consistency.py @@ -8,9 +8,9 @@ """ import sys +from pathlib import Path import pytest -from pathlib import Path # Ensure packages is importable sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "packages")) -- 2.54.0