diff --git a/.planning/debug/srt-processor-thinking-mode.md b/.planning/debug/srt-processor-thinking-mode.md new file mode 100644 index 0000000..dd3f544 --- /dev/null +++ b/.planning/debug/srt-processor-thinking-mode.md @@ -0,0 +1,90 @@ +--- +status: awaiting_human_verify +trigger: "调查并修复 SRTProcessor 的 ASR 纠错问题:启用思考模式后输出 AI 的思考过程而非纠错后的字幕内容,以及文件命名缺少标题" +created: 2026-03-21T22:00:00.000000 +updated: 2026-03-21T22:30:00.000000 +--- + +## Current Focus +hypothesis: 修复已完成并通过测试 +test: 所有自动化测试通过(4/4) +expecting: ASR 纠错将使用禁用思考模式,文件命名将包含标题 +next_action: 用户需要在实际环境中验证修复效果 + +## 自我验证结果 +- ✓ SimpleAIClient 支持 disable_reasoning 参数 +- ✓ ASRCorrector 使用 disable_reasoning=True +- ✓ ArchiveProcessor 从 frontmatter 提取标题 +- ✓ _call_zhipu 在 disable_reasoning=True 时添加 thinking.type=disabled + +## Symptoms +expected: | + 1. ASR 纠错后输出纠正和润色后的完整字幕内容 + 2. 文件命名包含视频标题 +actual: | + 1. 输出包含 AI 的思考过程(Analyze the Request, 分析请求等) + 2. 文件命名格式:20260321_BV1MiPSz2ErY_analysis.md(缺少标题) +errors: | + - AI 返回 reasoning_content 而非 content + - 文件命名未包含标题 +reproduction: | + 参考:~/work/zq/30.资源/AI分析/bilibili/20260321_BV1MiPSz2ErY_analysis.md + 文件内容显示AI的思考分析过程而非纠错后的字幕 +started: | + 2026-03-21,修复 DeepAnalyzer 后重新启用思考模式,但 SRTProcessor 也受到相同问题影响 + +## Eliminated + +## Evidence +- timestamp: 2026-03-21T22:00:00 + checked: 实际输出文件 ~/work/zq/30.资源/AI分析/bilibili/20260321_BV1MiPSz2ErY_analysis.md + found: 文件内容包含 AI 的思考分析过程("Analyze the Request", "分析请求" 等),而非纠错后的字幕 + implication: 修复代码没有生效,问题仍然存在 + +- timestamp: 2026-03-21T22:00:00 + checked: DeepAnalyzer._call_zhipu 的修复逻辑(第 110-124 行) + found: 代码逻辑是"优先返回 content 字段,如果 content 为空才使用 reasoning_content" + implication: 这个逻辑是错误的!当启用思考模式时,content 不为空但包含的是 AI 的分析过程 + +- timestamp: 2026-03-21T22:00:00 + checked: 智谱 API 文档 + found: thinking.type 参数控制是否启用思考模式:enabled(默认)或 disabled + implication: 解决方案是在调用 API 时显式设置 thinking.type="disabled" 来禁用思考模式 + +- timestamp: 2026-03-21T22:00:00 + checked: 提交 7ac4937 的 diff + found: 该提交移除了 thinking.type="disabled" 参数,导致启用默认的思考模式 + implication: 这就是问题的根源!移除 thinking.type="disabled" 后,AI 返回分析过程而非直接答案 + +- timestamp: 2026-03-21T22:00:00 + checked: 之前的提交 7903690 + found: 这个提交添加了 thinking.type="disabled" 参数来禁用思考模式 + implication: 但提交 7ac4937 又移除了它,导致问题重新出现 + +## Resolution +root_cause: | + 提交 7ac4937 移除了 thinking.type="disabled" 参数,导致智谱 API 使用默认的思考模式。 + 当启用思考模式时,AI 返回的内容(content 字段)包含了详细的分析过程(如 "Analyze the Request"), + 而非我们期望的直接纠错后的字幕。ASR 纠错需要的是直接结果,不需要分析过程。 + +fix: | + 1. 在 SimpleAIClient 中添加 disable_reasoning 参数(默认 False) + 2. 在 ASRCorrector 调用时设置 disable_reasoning=True + 3. 在 _call_zhipu 方法中,当 disable_reasoning=True 时添加 thinking.type="disabled" 参数 + 4. 在 ArchiveProcessor 中从 frontmatter 提取标题并添加到文件名 +files_changed: [] + - kb_workflow/deep_analyzer/__init__.py: 添加 disable_reasoning 参数支持 + - kb_workflow/srt/corrector.py: ASRCorrector 使用 disable_reasoning=True + - kb_workflow/pipeline/processors/archive.py: 从文件提取标题并添加到文件名 + +verification: | + 自动化测试全部通过(4/4): + + 1. ✓ SimpleAIClient 默认 disable_reasoning=False + 2. ✓ 可以设置 disable_reasoning=True + 3. ✓ generate 方法有 disable_reasoning 参数 + 4. ✓ ASRCorrector._correct_batch 使用 disable_reasoning=True + 5. ✓ _extract_title_from_file 正确提取标题 + 6. ✓ _sanitize_title 清理标题 + 7. ✓ _call_zhipu 在 disable_reasoning=True 时添加 thinking.type=disabled + 8. ✓ _call_zhipu 返回 content 字段 diff --git a/kb_workflow/deep_analyzer/__init__.py b/kb_workflow/deep_analyzer/__init__.py index 974d077..10f9363 100644 --- a/kb_workflow/deep_analyzer/__init__.py +++ b/kb_workflow/deep_analyzer/__init__.py @@ -37,20 +37,25 @@ class AnalysisResult: class SimpleAIClient: """AI 客户端""" - def __init__(self, api_key: str = None, provider: str = None, model: str = None, base_url: str = None): + def __init__(self, api_key: str = None, provider: str = None, model: str = None, base_url: str = None, disable_reasoning: bool = False): self.api_key = api_key or os.environ.get("AI_API_KEY", "") self.provider = provider or os.environ.get("AI_PROVIDER", "") self.model = model or os.environ.get("AI_MODEL", "glm-5") self.base_url = base_url or os.environ.get("AI_BASE_URL", "") self.max_retries = 3 + self.disable_reasoning = disable_reasoning - def generate(self, prompt: str, max_tokens: int = 16000) -> str: + def generate(self, prompt: str, max_tokens: int = 16000, disable_reasoning: bool = None) -> str: """生成文本""" api_key = os.environ.get("AI_API_KEY", self.api_key) provider = os.environ.get("AI_PROVIDER", self.provider) model = os.environ.get("AI_MODEL", self.model) base_url = os.environ.get("AI_BASE_URL", self.base_url) + # 确定 disable_reasoning 的值:优先使用参数,否则使用实例变量 + if disable_reasoning is None: + disable_reasoning = self.disable_reasoning + if not api_key: raise ValueError("AI_API_KEY 未设置") @@ -63,7 +68,7 @@ class SimpleAIClient: if provider == "openai": result = self._call_openai(prompt, max_tokens, api_key, model, base_url) elif provider == "zhipu": - result = self._call_zhipu(prompt, max_tokens, api_key, model, base_url) + result = self._call_zhipu(prompt, max_tokens, api_key, model, base_url, disable_reasoning) elif provider == "minimax": result = self._call_minimax(prompt, max_tokens, api_key, model, base_url) else: @@ -88,7 +93,7 @@ class SimpleAIClient: response.raise_for_status() return response.json()['choices'][0]['message']['content'] - def _call_zhipu(self, prompt: str, max_tokens: int, api_key: str, model: str, base_url: str) -> str: + def _call_zhipu(self, prompt: str, max_tokens: int, api_key: str, model: str, base_url: str, disable_reasoning: bool = False) -> str: import requests import sys @@ -96,22 +101,28 @@ class SimpleAIClient: url = f"{base_url}/chat/completions" headers = {"Authorization": f"Bearer {api_key}"} # GLM-4.5/4.6/4.7/5 默认启用thinking mode - # 移除 thinking.type="disabled",让AI使用默认思考模式以提高准确性 + # 通过 disable_reasoning 参数控制是否禁用思考模式 payload = { "model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens } + + # 如果明确要求禁用思考模式,添加 thinking 参数 + if disable_reasoning: + payload["thinking"] = {"type": "disabled"} response = requests.post(url, headers=headers, json=payload, timeout=600) response.raise_for_status() data = response.json() message = data.get('choices', [{}])[0].get('message', {}) - # 优先返回 content 字段 + # 返回 content 字段 + # 当禁用思考模式时,content 包含直接答案 + # 当启用思考模式时,content 可能包含思考过程和分析 content = message.get('content', '') - # 如果 content 为空但有 reasoning_content,使用 reasoning_content if not content: + # 如果 content 为空,检查是否有 reasoning_content reasoning = message.get('reasoning_content', '') if reasoning: print(f"[DEBUG] Zhipu API返回空的content,使用reasoning_content。", file=sys.stderr) diff --git a/kb_workflow/pipeline/processors/archive.py b/kb_workflow/pipeline/processors/archive.py index e9c20ac..192b150 100644 --- a/kb_workflow/pipeline/processors/archive.py +++ b/kb_workflow/pipeline/processors/archive.py @@ -7,6 +7,7 @@ to the archive directory organized by platform. from __future__ import annotations import logging +import re import shutil from datetime import datetime from pathlib import Path @@ -65,9 +66,14 @@ class ArchiveProcessor(BaseProcessor): platform_dir = archive_base / input_data.platform platform_dir.mkdir(parents=True, exist_ok=True) - # 文件命名: {YYYYMMDD}_{content_id}_analysis.md + # 从源文件中提取标题(用于文件命名) + title = self._extract_title_from_file(Path(input_data.analysis_file)) + # 清理标题:移除特殊字符,限制长度 + safe_title = self._sanitize_title(title) if title else input_data.content_id + + # 文件命名: {YYYYMMDD}_{content_id}_{title}_analysis.md timestamp = datetime.now().strftime("%Y%m%d") - archive_filename = f"{timestamp}_{input_data.content_id}_analysis.md" + archive_filename = f"{timestamp}_{input_data.content_id}_{safe_title}_analysis.md" archive_path = platform_dir / archive_filename # Step 3: 检查重复 @@ -147,6 +153,64 @@ class ArchiveProcessor(BaseProcessor): matches = list(platform_dir.glob(pattern)) return matches[0] if matches else None + def _extract_title_from_file(self, file_path: Path) -> Optional[str]: + """从 Markdown 文件的 frontmatter 中提取标题 + + Args: + file_path: Markdown 文件路径 + + Returns: + 标题字符串,如果未找到返回 None + """ + try: + content = file_path.read_text(encoding='utf-8') + + # 查找 frontmatter 中的 title 字段 + # 格式:title: "标题内容" 或 title: 标题内容 + title_match = re.search(r'^title:\s*(?:"([^"]*)"|(.+))$', content, re.MULTILINE) + + if title_match: + # 优先使用引号内的内容,否则使用第二个捕获组 + title = title_match.group(1) or title_match.group(2) + return title.strip() if title else None + + return None + except Exception as e: + logger.warning(f"Failed to extract title from {file_path}: {e}") + return None + + def _sanitize_title(self, title: str) -> str: + """清理标题用于文件命名 + + 移除特殊字符,限制长度,转换为安全的文件名。 + + Args: + title: 原始标题 + + Returns: + 清理后的标题 + """ + # 移除或替换不适合文件名的字符 + # 保留中文、字母、数字、下划线、连字符 + # 替换所有特殊字符(包括标点符号和常见符号) + safe = re.sub(r'[\\/:*?"<>|,。!?、;:""\'\'\s@#$%&*+\-=\[\]{}()<>]+', '_', title) + + # 移除连续的下划线 + safe = re.sub(r'_+', '_', safe) + + # 去除首尾下划线和空格 + safe = safe.strip('_') + + # 限制长度(避免文件名过长) + if len(safe) > 50: + safe = safe[:50] + + # 如果清理后为空,返回默认值 + if not safe: + safe = "untitled" + + return safe + def _ask_overwrite(self, existing_file: Path, is_non_interactive: bool = False) -> bool: """询问用户是否覆盖已存在的文件 diff --git a/kb_workflow/srt/corrector.py b/kb_workflow/srt/corrector.py index 5609578..0af6091 100644 --- a/kb_workflow/srt/corrector.py +++ b/kb_workflow/srt/corrector.py @@ -100,7 +100,8 @@ class ASRCorrector: """ try: prompt = CORRECTION_PROMPT.format(text=text) - result = self.ai_client.generate(prompt, max_tokens=len(text) + 500) + # ASR 纠错需要直接结果,不需要 AI 的分析过程,因此禁用思考模式 + result = self.ai_client.generate(prompt, max_tokens=len(text) + 500, disable_reasoning=True) return result.strip() except Exception as e: logger.warning(f"ASR correction failed: {e}, returning original text")