fix(srt): add disable_reasoning parameter for ASR correction and include title in archive filename
PR Test / lint (pull_request) Successful in 1s
PR Test / lint (pull_request) Successful in 1s
- SimpleAIClient: Add disable_reasoning parameter to control AI thinking mode
- ASRCorrector: Use disable_reasoning=True to get direct correction results
- ArchiveProcessor: Extract title from frontmatter and include in filename
File naming format: {YYYYMMDD}_{content_id}_{title}_analysis.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 字段
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
"""询问用户是否覆盖已存在的文件
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user