From 022b8bbea3d48f0f60201dd27635ab78ea6ad86a Mon Sep 17 00:00:00 2001 From: linyq-laien Date: Sat, 18 Jul 2026 04:26:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(webui,=20llm,=20i18n):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=8F=AF=E8=87=AA=E5=AE=9A=E4=B9=89=E7=9A=84=E8=A7=A3?= =?UTF-8?q?=E8=AF=B4=E6=96=87=E6=A1=88=E5=AD=97=E6=95=B0=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在WebUI脚本设置页面新增字数输入控件,支持100-5000区间的目标字数设置 更新LLM提示词模板,使用用户指定字数作为目标(±10%浮动)替代原固定范围 补充中英文国际化翻译文案 升级项目版本至0.8.7 更新相关服务函数与单元测试适配新参数 --- app/services/SDE/short_drama_explanation.py | 4 ++++ app/services/llm/migration_adapter.py | 2 ++ ...test_subtitle_adapter_pipeline_unittest.py | 6 ++++++ .../film_tv_narration/narration_copy.py | 16 ++++++++++++-- .../short_drama_narration/narration_copy.py | 16 ++++++++++++-- project_version | 2 +- webui/components/script_settings.py | 21 ++++++++++++++++++- webui/i18n/en.json | 1 + webui/i18n/zh.json | 1 + webui/tools/generate_short_summary.py | 3 +++ 10 files changed, 66 insertions(+), 6 deletions(-) diff --git a/app/services/SDE/short_drama_explanation.py b/app/services/SDE/short_drama_explanation.py index 5d85679..c77dc8c 100644 --- a/app/services/SDE/short_drama_explanation.py +++ b/app/services/SDE/short_drama_explanation.py @@ -409,6 +409,7 @@ class SubtitleAnalyzer: temperature: float = 0.7, narration_language: str = "简体中文(中国)", drama_genre: str = "逆袭/复仇", + narration_word_count: int = 500, ) -> Dict[str, Any]: """生成供用户审核修改的解说正文。""" try: @@ -420,6 +421,7 @@ class SubtitleAnalyzer: "plot_analysis": plot_analysis, "subtitle_content": subtitle_content, "narration_language": narration_language, + "narration_word_count": int(narration_word_count), }, ) return self._generate_plain_text(prompt, system_prompt, temperature) @@ -964,6 +966,7 @@ def generate_narration_copy( narration_language: str = "简体中文(中国)", drama_genre: str = "逆袭/复仇", prompt_category: str = "short_drama_narration", + narration_word_count: int = 500, ) -> Dict[str, Any]: """生成可供用户审核修改的解说正文。""" analyzer = SubtitleAnalyzer( @@ -982,6 +985,7 @@ def generate_narration_copy( temperature=temperature, narration_language=narration_language, drama_genre=drama_genre, + narration_word_count=narration_word_count, ) diff --git a/app/services/llm/migration_adapter.py b/app/services/llm/migration_adapter.py index aec7ab5..809725c 100644 --- a/app/services/llm/migration_adapter.py +++ b/app/services/llm/migration_adapter.py @@ -292,6 +292,7 @@ class SubtitleAnalyzerAdapter: temperature: float = 0.7, narration_language: str = "简体中文(中国)", drama_genre: str = "逆袭/复仇", + narration_word_count: int = 500, ) -> Dict[str, Any]: """Generate editable narration copy before timeline matching.""" try: @@ -303,6 +304,7 @@ class SubtitleAnalyzerAdapter: "plot_analysis": plot_analysis, "subtitle_content": subtitle_content, "narration_language": narration_language, + "narration_word_count": int(narration_word_count), }, ) narration_copy = self._generate_plain_text(prompt, system_prompt, temperature) diff --git a/app/services/llm/test_subtitle_adapter_pipeline_unittest.py b/app/services/llm/test_subtitle_adapter_pipeline_unittest.py index 77087e6..7d390b5 100644 --- a/app/services/llm/test_subtitle_adapter_pipeline_unittest.py +++ b/app/services/llm/test_subtitle_adapter_pipeline_unittest.py @@ -24,11 +24,14 @@ class SubtitleAnalyzerAdapterPipelineTests(unittest.TestCase): temperature=0.7, narration_language="简体中文(中国)", drama_genre="家庭伦理", + narration_word_count=800, ) self.assertEqual("success", result["status"]) self.assertIn("反击", result["narration_copy"]) self.assertIn("家庭伦理", call.call_args.kwargs["prompt"]) + self.assertIn("800", call.call_args.kwargs["prompt"]) + self.assertNotIn("300-650", call.call_args.kwargs["prompt"]) self.assertNotIn("response_format", call.call_args.kwargs) def test_generate_narration_copy_can_use_film_tv_prompt_category(self): @@ -49,11 +52,14 @@ class SubtitleAnalyzerAdapterPipelineTests(unittest.TestCase): temperature=0.7, narration_language="简体中文(中国)", drama_genre="悬疑/犯罪", + narration_word_count=1200, ) self.assertEqual("success", result["status"]) self.assertIn("影视解说正文创作任务", call.call_args.kwargs["prompt"]) self.assertIn("用户选择的影视类型", call.call_args.kwargs["prompt"]) + self.assertIn("1200", call.call_args.kwargs["prompt"]) + self.assertNotIn("350-750", call.call_args.kwargs["prompt"]) self.assertNotIn("短剧解说正文创作任务", call.call_args.kwargs["prompt"]) def test_film_tv_script_prompts_exclude_intro_outro_and_ads(self): diff --git a/app/services/prompts/film_tv_narration/narration_copy.py b/app/services/prompts/film_tv_narration/narration_copy.py index 7c6182a..1304a7e 100644 --- a/app/services/prompts/film_tv_narration/narration_copy.py +++ b/app/services/prompts/film_tv_narration/narration_copy.py @@ -22,7 +22,14 @@ class NarrationCopyPrompt(ParameterizedPrompt): model_type=ModelType.TEXT, output_format=OutputFormat.TEXT, tags=["影视", "解说文案", "电影解说", "剧情承接", "用户审核"], - parameters=["drama_name", "drama_genre", "plot_analysis", "subtitle_content", "narration_language"], + parameters=[ + "drama_name", + "drama_genre", + "plot_analysis", + "subtitle_content", + "narration_language", + "narration_word_count", + ], ) super().__init__(metadata, required_parameters=["drama_name", "plot_analysis", "subtitle_content"]) @@ -57,6 +64,11 @@ ${narration_language} ${drama_genre} +## 用户要求的文案字数 + +${narration_word_count} + + ## 类型写作规则 必须按用户选择的影视类型调整表达重点,不要自行改判类型: - 剧情/情感:突出人物选择、关系裂痕、命运压力和情绪余波。 @@ -81,7 +93,7 @@ ${drama_genre} 4. 每句话只表达一个信息点,适合后续按句匹配画面。 5. 句子尽量短,单句优先 15-35 字;信息复杂时拆成多句。 6. 每 2-3 句要有明确承接,让观众知道为什么从上一幕来到下一幕。 -7. 总长度控制在 350-750 字;短素材取下限,长素材取上限。 +7. 总长度以 ${narration_word_count} 字为目标,允许上下浮动 10%;中日韩语言按非空白字符计数,其他语言按单词计数。不得再套用固定长度区间。 8. 不要使用编号、项目符号、章节标题或括号说明。 ## 输出要求 diff --git a/app/services/prompts/short_drama_narration/narration_copy.py b/app/services/prompts/short_drama_narration/narration_copy.py index 362e0fd..3678bc0 100644 --- a/app/services/prompts/short_drama_narration/narration_copy.py +++ b/app/services/prompts/short_drama_narration/narration_copy.py @@ -22,7 +22,14 @@ class NarrationCopyPrompt(ParameterizedPrompt): model_type=ModelType.TEXT, output_format=OutputFormat.TEXT, tags=["短剧", "解说文案", "爆款开头", "叙事连续性", "用户审核"], - parameters=["drama_name", "drama_genre", "plot_analysis", "subtitle_content", "narration_language"], + parameters=[ + "drama_name", + "drama_genre", + "plot_analysis", + "subtitle_content", + "narration_language", + "narration_word_count", + ], ) super().__init__(metadata, required_parameters=["drama_name", "plot_analysis", "subtitle_content"]) @@ -57,6 +64,11 @@ ${narration_language} ${drama_genre} +## 用户要求的文案字数 + +${narration_word_count} + + ## 类型写作规则 必须按用户选择的短剧类型调整表达重点,不要自行改判类型: - 霸总/甜宠:突出误会、身份差、暧昧拉扯、守护感和情绪反差。 @@ -81,7 +93,7 @@ ${drama_genre} 4. 每句话只表达一个信息点,适合后续按句匹配画面。 5. 句子尽量短,单句优先 15-35 字;信息复杂时拆成多句。 6. 每 2-3 句要有明确因果承接,让观众知道为什么从上一幕来到下一幕。 -7. 总长度控制在 300-650 字;短素材取下限,长素材取上限。 +7. 总长度以 ${narration_word_count} 字为目标,允许上下浮动 10%;中日韩语言按非空白字符计数,其他语言按单词计数。不得再套用固定长度区间。 8. 不要使用编号、项目符号、章节标题或括号说明。 ## 输出要求 diff --git a/project_version b/project_version index 120f532..35864a9 100644 --- a/project_version +++ b/project_version @@ -1 +1 @@ -0.8.6 \ No newline at end of file +0.8.7 \ No newline at end of file diff --git a/webui/components/script_settings.py b/webui/components/script_settings.py index 3f1b666..163d3cf 100644 --- a/webui/components/script_settings.py +++ b/webui/components/script_settings.py @@ -97,6 +97,9 @@ FILM_TV_TYPE_VALUES = { "horror_thriller": "恐怖/惊悚", } SHORT_DRAMA_ORIGINAL_SOUND_RATIO_OPTIONS = list(range(0, 100, 10)) +DEFAULT_NARRATION_WORD_COUNT = 500 +MIN_NARRATION_WORD_COUNT = 100 +MAX_NARRATION_WORD_COUNT = 5000 SUMMARY_MODE_CONFIGS = { MODE_FILM_SUMMARY: { "mode_label_key": "Film TV Narration", @@ -1574,6 +1577,7 @@ def render_script_buttons(tr, params): type_option_key = _summary_state_key(summary_config, "type_option") custom_type_key = _summary_state_key(summary_config, "custom_type") original_sound_ratio_key = _summary_state_key(summary_config, "original_sound_ratio") + narration_word_count_key = _summary_state_key(summary_config, "narration_word_count") language_option_key = _summary_state_key(summary_config, "narration_language_option") custom_language_key = _summary_state_key(summary_config, "custom_narration_language") narration_copy_key = _summary_state_key(summary_config, "narration_copy") @@ -1592,7 +1596,7 @@ def render_script_buttons(tr, params): config_col_widths = [1.15] if show_custom_type: config_col_widths.append(1.15) - config_col_widths.extend([0.9, 1.15]) + config_col_widths.extend([0.9, 0.9, 1.15]) if show_custom_language: config_col_widths.append(1.15) @@ -1623,6 +1627,16 @@ def render_script_buttons(tr, params): key=original_sound_ratio_key, ) config_col_index += 1 + with config_cols[config_col_index]: + st.number_input( + tr("文案字数"), + min_value=MIN_NARRATION_WORD_COUNT, + max_value=MAX_NARRATION_WORD_COUNT, + value=DEFAULT_NARRATION_WORD_COUNT, + step=50, + key=narration_word_count_key, + ) + config_col_index += 1 with config_cols[config_col_index]: st.selectbox( tr("解说语言"), @@ -1663,6 +1677,7 @@ def render_script_buttons(tr, params): type_option_key = _summary_state_key(summary_config, "type_option") custom_type_key = _summary_state_key(summary_config, "custom_type") original_sound_ratio_key = _summary_state_key(summary_config, "original_sound_ratio") + narration_word_count_key = _summary_state_key(summary_config, "narration_word_count") language_option_key = _summary_state_key(summary_config, "narration_language_option") custom_language_key = _summary_state_key(summary_config, "custom_narration_language") narration_copy_key = _summary_state_key(summary_config, "narration_copy") @@ -1674,6 +1689,9 @@ def render_script_buttons(tr, params): narration_language = _resolve_summary_narration_language(summary_config) drama_genre = _resolve_summary_type(summary_config) original_sound_ratio = int(st.session_state.get(original_sound_ratio_key, 30)) + narration_word_count = int( + st.session_state.get(narration_word_count_key, DEFAULT_NARRATION_WORD_COUNT) + ) if ( st.session_state.get(type_option_key) == "custom" and not str(st.session_state.get(custom_type_key, '') or '').strip() @@ -1720,6 +1738,7 @@ def render_script_buttons(tr, params): video_paths=_selected_video_paths(), narration_language=narration_language, drama_genre=drama_genre, + narration_word_count=narration_word_count, prompt_category=summary_config["prompt_category"], search_keywords=summary_config["search_keywords"], empty_title_message_key=summary_config["empty_title_message_key"], diff --git a/webui/i18n/en.json b/webui/i18n/en.json index b3b1a27..90f6e73 100644 --- a/webui/i18n/en.json +++ b/webui/i18n/en.json @@ -341,6 +341,7 @@ "影视类型": "Film/TV Type", "自定义影视类型": "Custom Film/TV Type", "原片占比": "Original Footage Ratio", + "文案字数": "Copy Length", "例如:豪门虐恋": "For example: billionaire angst romance", "例如:悬疑犯罪": "For example: suspense crime", "请输入自定义短剧类型": "Please enter a custom short drama type", diff --git a/webui/i18n/zh.json b/webui/i18n/zh.json index bdf3fba..f6cf688 100644 --- a/webui/i18n/zh.json +++ b/webui/i18n/zh.json @@ -703,6 +703,7 @@ "影视类型": "影视类型", "自定义影视类型": "自定义影视类型", "原片占比": "原片占比", + "文案字数": "文案字数", "例如:豪门虐恋": "例如:豪门虐恋", "例如:悬疑犯罪": "例如:悬疑犯罪", "请输入自定义短剧类型": "请输入自定义短剧类型", diff --git a/webui/tools/generate_short_summary.py b/webui/tools/generate_short_summary.py index df1bf54..86c240f 100644 --- a/webui/tools/generate_short_summary.py +++ b/webui/tools/generate_short_summary.py @@ -363,6 +363,7 @@ def generate_short_drama_narration_copy( search_keywords: str = SHORT_DRAMA_SEARCH_KEYWORDS, empty_title_message_key: str = "Please enter short drama name before web search", web_search_context_description: str = "短剧名称、人物关系、剧情背景和公开剧情梗概", + narration_word_count: int = 500, ): """生成可由用户审核修改的短剧解说正文,不绑定时间戳。""" subtitle_paths = _normalize_paths(subtitle_path) @@ -422,6 +423,7 @@ def generate_short_drama_narration_copy( temperature=temperature, narration_language=narration_language, drama_genre=drama_genre, + narration_word_count=narration_word_count, ) except Exception as e: logger.warning(f"使用新LLM服务生成文案失败,回退到旧实现: {str(e)}") @@ -436,6 +438,7 @@ def generate_short_drama_narration_copy( provider=text_provider, narration_language=narration_language, drama_genre=drama_genre, + narration_word_count=narration_word_count, prompt_category=prompt_category, )