docs: 注释规范要求精简可读,并清理生产代码中的历史叙事

AGENTS.md 的注释规范新增三节可执行约束:

- 只写代码真实逻辑:注释只回答"做什么"与"为什么必须这么做",禁止写决策/
  修改时间、历史版本对比、实测数据与实验结论、事故与缺陷编号(run_xxxx /
  batch_xxxx / R01 等)——这些属 docs/decisions.md 与审查跟踪文件;当前生效
  的约束可以写,但不附带它何时因何变成这样。
- 精简可读:单段连续注释不超过 3 行;docstring 一句话概括职责,不重复函数名
  已表达的信息;不写逐行翻译代码的废话注释,只在非显然处(业务规则、边界、
  易错点、外部约束)加注。
- 覆盖范围:测试注释只说明验证什么行为,回归用例可保留一句溯源;并明确
  参数说明应写在**参数读取处**附近,而不是把多个参数的解释堆在离使用位置
  很远的注释块里。

按此清理生产代码(注释净减 70 行,18 个文件),典型处理:

- nodes/whisper.py:删掉堆在一起、含"用户 2026-08 决定 / 实测 savr-1054"
  等叙事的参数块,把各参数说明移到各自的读取处与 model.transcribe 调用处;
- nodes/llm_filter.py、nodes/subtitle_cleanup.py:模块 docstring 去掉英文
  背景叙事与条数统计,保留"默认只跑规则层""整条删除而非 '-' 占位"等当前
  行为;
- src/wov_app/{batch,db,scheduler}.py 与 routers:去掉 batch_xxx/run_xxx 事故
  编号与"修复前……"对比,改为一句"否则会出现什么问题";
- nodes/ass.py、frame_extract.py:去掉废弃值对比与日期,保留判据本身。

安全验证:用 AST 对比(剥离 docstring 后比较语法树)确认 18 个文件**零逻辑
变更**;`nodes/proper_nouns.py` 的规则表 reason 字段会注入 LLM 提示词,属于
数据而非注释,已恢复原值。全量测试 476 passed。
This commit is contained in:
2026-09-13 16:37:49 +08:00
parent 8f6083f8cf
commit 7a7212f70c
18 changed files with 192 additions and 262 deletions
+25 -36
View File
@@ -1,31 +1,23 @@
"""LLM 字幕过滤节点。
"""LLM 字幕过滤节点:对 OCR 识别出的 SRT 做二次过滤
对 OCR 识别出的 SRT 字幕做二次过滤,两级判断:
两级判断,**默认只跑规则层**(use_llm=0;LLM 层误删真实对话的代价过高,
依据见 docs/decisions.md):
1. **确定性规则层**(不调 LLM):横线装饰、HTML/水印 token、URL/邮箱、
单双 ASCII 字符等 OCR 噪声直接删除——这些模式稳定可判的,走规则
既省 token 又保证结果确定(真实数据中占删除量的 65%+)。
1. **确定性规则层**(不调 LLM):横线装饰、HTML/水印 token、URL/邮箱、
单双 ASCII 字符等 OCR 噪声直接删除模式稳定可判,省 token 且结果确定。
2. **LLM 五类分类层**:把目标字幕连同前后各 context_size 条纯文本分批
提供给 LLM模型输出五个类别之一:
- garbage:垃圾字符(乱码残缺装饰性符号)→ 删除
- overlay:水印/网页/播放器等覆盖层文本 → 删除
- noise:与上下文无关、无实际语义的杂项 → 删除
- repeat:内容性重复(语气词、呻吟、重复感叹,属于内容本身)→ 保留
- dialogue:正常对话 → 保留
未识别输出一律回退 dialogue(宁滥勿缺,避免误删真实对话)。
2. **LLM 五类分类层**use_llm=1 时启用):把目标字幕连同前后各
context_size 条纯文本交给 LLM,输出五个类别之一:
- garbage(乱码/残缺/装饰)、overlay(水印/播放器覆盖层)、
noise(无实义杂项)→ 删除
- repeat(内容性重复,如语气词/呻吟)、dialogue(正常对话)→ 保留。
未识别输出一律回退 dialogue(宁滥勿缺)。
3. **文本去重**:相同文本(忽略全部空白与大小写差异)只调一次 LLM,
上下文取首次出现位置,结果缓存复用——修复"同一句字幕 5 留 6 删"
判定一致,同时把长视频的 LLM 调用量降到唯一文本数。
4. **上下文净化**:喂给 LLM 的上下文是**过滤后的字幕**——规则层确定性的
垃圾(横线/HTML/网址/水印等)从上下文中剔除,只留下有意义的对白,
避免覆盖层垃圾污染 LLM 的场景判断导致误删真实对话。
参考真实任务 run_ac7f480a3ccb2026-08OCR 1666 条):旧实现把 394 条
真实对话当噪声删掉(占删除 35%)、同文本判定不一致;新实现按上述机制
回归测试已固化在 tests/test_llm_filter.py。
该层的两个配套机制:
- **文本去重**:相同文本(忽略空白与大小写)只调一次 LLM,结果复用,
保证同一句话判定一致并减少调用量;
- **上下文净化**:喂给 LLM 的上下文先剔除规则层已判定的垃圾,避免覆盖层
噪声污染场景判断而误删真实对话。
"""
from __future__ import annotations
@@ -120,7 +112,7 @@ _HTML_MARK_RE = re.compile(
re.IGNORECASE,
)
# 规则层正则:播放器/作品编号水印(VLM 常把画面角落的编号识别成短串)。
# 实测真实数据命中:SPHO-1 / PHO一号馆 / NO.1专用 / PHD-手術 / SP10-1型。
# 覆盖水印编号形态,如 SPHO-1 / PHO一号馆 / NO.1专用 / PHD-手術 / SP10-1型。
# 限定为**不含汉字的编号形态**(或纯形态串),避免误伤正常英文对白。
_SERIAL_MARK_RE = re.compile(
r"^(?:SPH|SPHO|SPIO|SPNO|SP10|PHO|PH0|PHD|P10|NO\.|SP\s*\d)"
@@ -142,14 +134,14 @@ _CAST_MARK_RE = re.compile(r"^[(]\s*(?:出演|主演|配役|监督|スタッ
DEFAULT_OVERLAY_TOKENS = frozenset(
{"html", "background", "___", "cleaning", "buffering", "loading", "marketing"}
)
# 长文本保护阈值:≥ 该长度的文本,noise 类别不构成删除依据
# LLM 判定不稳定(实测把完整对话句误判 noise,长度是必要兜底而非删除依据
# 长文本保护阈值:≥ 该长度的文本,noise 类别不构成删除依据LLM 会把完整
# 对话句误判 noise,长度是兜底手段,只用于保护不用于删除)
DEFAULT_MIN_KEEP_LEN = 12
# 纯数字/小数点组合("4.0"/"2.0"/"10-1期B" 中的纯数值形态)。
_NUMBER_ONLY_RE = re.compile(r"^\d{1,3}[.,]\d{1,2}$")
# LLM 分类层开关(2026-09 默认关闭):见模块与 nodes/llm_filter.py 说明
# LLM 分类层开关:默认关闭,只跑确定性规则层(原因见模块 docstring)
DEFAULT_USE_LLM = False
@@ -232,9 +224,8 @@ def _should_delete(category: str, text: str, min_keep_len: int) -> bool:
"""按 LLM 类别与长文本保护决定是否删除。
repeat/dialogue 一律保留;garbage/overlay 一律删除(明确的垃圾信号);
noise 对短文本删除,但 ≥min_keep_len 的长文本不删——LLM 判定不稳定,
完整对话句常被误判 noise,长度保护是必要兜底(实测移除后新增误删
124 条真实长对话)。长度只用于"保护",不用于"删除"
noise 对短文本删除:LLM 会把完整对话句误判为 noise,≥min_keep_len 的
长文本一律保留。长度只用于保护,不用于删除。
"""
if category not in DELETE_CATEGORIES:
return False
@@ -362,10 +353,8 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
}
# 去重开关:默认开;关掉时每个条目独立调用 LLM(不省调用,判定各自独立)。
dedupe = str(request.params.get("dedupe", "1")) not in ("0", "false", "False")
# LLM 分类层开关(2026-09 默认关闭):实测该层额外删除的 131 条中 56% 是
# 真实对话(run_ac7f480a3ccb 逐类人工审查),真正抓到而规则层抓不到的仅 58
# 条(已大部分下沉为规则)。默认只跑确定性规则层,宁多留不漏删;
# 需要旧行为时用 params.use_llm=1 显式打开。
# LLM 分类层开关:默认关闭,只跑确定性规则层(宁多留不漏删);关闭原因
# 见模块 docstring 与 docs/decisions.md。需要该层时用 params.use_llm=1。
use_llm = str(request.params.get("use_llm", "1" if DEFAULT_USE_LLM else "0")) not in (
"0", "false", "False",
)
@@ -414,7 +403,7 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
done, total, rate, avg_time, workers, pool.max_workers,
)
# 自适应并发调用 LLM:按实测负载弹性伸缩,避免压垮 LLM 接口。
# 自适应并发调用 LLM:按响应快慢弹性伸缩,避免压垮接口。
pool = AdaptiveThreadPool(
worker=judge_one,
on_progress=log_progress,