docs: 记录模型切换与过滤层决策,新增等待规则

- AGENTS.md:
  * LLM_MODEL 更新为 Qwen/Qwen3.5-35B-A3B,并说明 subtitle-correction
    节点有意保留旧模型(实测 0/4 vs 4/4);
  * llm-filter 节点表格改写:规则层为默认且唯一启用层级,列出新增的
    水印编号/日期/VLM 提示回显/角色标注规则,并记录 LLM 层关闭的
    数据依据(131 条额外删除中 56% 是真对话);
  * **新增「等待规则(强制,2026-09)」**:单次等待不得超过 10 秒,
    长任务放后台写日志并每 10 秒轮询;明确禁止 sleep 600 /
    timeout 1800 这类阻塞用户的写法(含反例说明)。
- README.md:同步默认模型、纠错节点例外、过滤层默认关闭;
- scripts/probe_llm_rate_limit.py 与 tests/test_integration_prompt_rules.py
  的兜底默认值同步为 Qwen/Qwen3.5-35B-A3B。
This commit is contained in:
2026-09-13 10:15:50 +08:00
parent e98f90e164
commit fc28f22a3f
3 changed files with 27 additions and 4 deletions
+16 -2
View File
@@ -14,6 +14,20 @@ ASS)在**同一个进程**内运行,不再启动子进程、不再走节点
- 需要用户确认的场景:新增功能、修复、文档改动、工作流/清单数据改动等
一切涉及 git 写操作的行为。
## 等待规则(强制,2026-09
- **单次等待时间不得超过 10 秒**。需要长时间运行时,把它放到后台
`setsid ... &` / `nohup ... &`)写日志,然后**每 10 秒轮询一次结果**
`for i in $(seq 1 6); do sleep 10; ...; done`),而不是用一个长 `sleep`
或长 `timeout` 把用户的对话阻塞住。
- 反例(禁止):`sleep 600``timeout 1800 uv run pytest`——即使命令本身
很快,把 timeout 设成 30 分钟也是错的:它无法预判耗时,只会在出问题时
让用户白等半小时。应当先用 10 秒轮询确认它是否结束。
- 正例:把任务丢后台 → `for i in $(seq 1 N); do sleep 10; <检查是否完成 && break>; done`
耗时任务写日志到 `data/experiments/...`,事后读日志汇报。
- 测试与构建也遵循此规则:已知 `uv run pytest` 全量约 70 秒,用后台+轮询,
不要写 `timeout 1800`
## 架构概览
```
@@ -60,7 +74,7 @@ vrsub/
| `vlm-ocr` | `image_uri` | `text``text_uri` | 直接调本地 Ollama 多模态模型(glm-ocr)的 `/api/chat` 做视频帧 OCR(流式 + 5s 上限),参数:`model``ollama_host``prompt``timeout_seconds``keep_alive``num_predict``temperature``repeat_penalty` |
| `frame-extract` | `video_uri` | `frames_manifest``frame_count` | 按**帧间隔**抽帧(解析 fps → step=round(间隔秒×fps)ffmpeg select 按帧号精确取帧,帧时间=帧号/fps 无累计偏差)并 crop 裁切字幕区域,参数:`interval_seconds`(默认 0.5)、`crop`[x,y,w,h] 0~1**默认画面底部 1/4** `[0,0.75,1,0.25]`——字幕很少出现在画面上半部分,2026-08 调整)。**帧文件必须按帧号数值排序读取**(`_sorted_frame_files`):ffmpeg `%04d` 编号超过 9999 帧后扩为 5 位,字典序 `sorted()` 会把 5 位编号排在 4 位之前导致时间与图像错位(真实发生于 run_339ec7ee437f 的 14236 帧任务,回归测试见 `test_frame_files_read_order_matches_frame_number` |
| `subtitle-ocr` | `frames_manifest` | `srt_uri``count` | 自适应线程池并发逐帧调 vlm-ocr → 垃圾过滤(无文字帧)→ 相同字幕合并(记录最后可见帧)→ 组装 SRT,消失时间=最后可见帧+采样间隔(间隔从帧清单推导),参数:`min_chars``min_alnum_ratio``garbage_tokens``pool_min_workers`/`pool_max_workers`/`pool_window_seconds`/`pool_fast_threshold`/`pool_slow_threshold` |
| `llm-filter` | `srt_uri` | `srt_uri``kept``removed` | 两级过滤:①**规则层**(不调 LLM)正则确定性删除——横线装饰、URL/邮箱/**裸网址域名**(含中文夹杂的注册地址)、**HTML/水印模式**html code/标签/javascript 等)、overlay tokenhtml/marketing 等)、单双 ASCII 字符;②**LLM 五类分类**garbage/overlay/noise 删,repeat/dialogue 留,未识别回退保留)每条连同前后各 `context_size`默认 10条纯文本分批判断——**上下文净化**:喂给 LLM 的是**过滤后的字幕**,规则层确定性垃圾从上下文中剔除(原文不进 LLM),避免覆盖层垃圾污染场景判断误删真对话(回归:run_011d01f19999 曾 190 条含 ≥4 汉字对话被误删);**长文本保护**:≥`min_keep_len`(默认 12)时 noise 不构成删除依据——LLM 判定不稳定,长度是必要兜底(实测移除保护后新增误删 124 条真实长对话)。**限流自适应**:LLM 调用 429/5xx 指数退避重试(最多 3 次,1s/2s/4s),worker 捕获限流错误时调用线程池 `report_failure()` **内存中临时降低最大线程数并缩容**(连续无错误窗口后逐步回升),失败条目在收紧后的并发下**重试一轮**,二次仍失败才整体失败——20 并发一拥而上触发 429 时自动收敛到配额内而不打挂任务。**节点级断点存档**(2026-08):每条判定成功即追加 `filter_partial.jsonl``{"index","category"}`,多线程加锁串行化),失败/中断后重跑只重判未判定条目,已判定结果复用(与 OCR 存档同机制)。**按文本去重**(忽略空白/大小写,相同文本只调一次 LLM,上下文取首次出现)保证判定一致并省调用。参数:`context_size``min_keep_len``overlay_tokens`JSON 数组)、`dedupe`(默认开)、`model``pool_min_workers`/`pool_max_workers`/`pool_window_seconds`/`pool_fast_threshold`/`pool_slow_threshold`。回归数据:testdata/ocr_srt_run_ac7f480a3ccb.srt(真实任务 1666 条 OCR 输出) |
| `llm-filter` | `srt_uri` | `srt_uri``kept``removed` | 两级过滤:①**规则层**(不调 LLM,**2026-09 起为默认且唯一启用的层级**)正则确定性删除——横线装饰、URL/邮箱/**裸网址域名**(含中文夹杂的注册地址)、**HTML/水印模式**html code/标签/javascript 等)、overlay tokenhtml/marketing 等)、单双 ASCII 字符、**水印编号**SPHO-1/PHO一号馆/NO.1专用)、**日期/数值**2011-11-27/4.0)、**VLM 提示回显**no text is visible)、**角色标注**((出演));②**LLM 五类分类**garbage/overlay/noise 删,repeat/dialogue 留)—— **默认关闭**`use_llm`默认 0,需显式 `use_llm=1` 才启用。**关闭原因(run_ac7f480a3ccb 逐类人工审查)**LLM 层额外删除的 131 条中 **56%73 条)是真实对话**`好好教育她一番吧`/`腿不要合上`/`这家医院 为VIP患者提供了特殊服务`),而它真正抓住而规则层抓不到的仅 58 条且大半可正则化(已下沉);`repeat` 类别 67 条判定零删除;长文本保护等五套机制全在给不稳定分类器兜底。关闭后真实数据保留 863 条(旧 588 条)、**误删真对话 0 条**(旧 73 条)、无 LLM 调用。启用时保留原机制:每条连同前后各 `context_size`(默认 10)条**过滤后**文本判断(上下文净化),≥`min_keep_len`(默认 12)时 noise 不构成删除依据(长文本保护),429/5xx 指数退避 + 自适应线程池 `report_failure()` 降并发后重试一轮,判定成功即追加 `filter_partial.jsonl` 断点存档;按文本去重。参数:`context_size``min_keep_len``overlay_tokens`JSON 数组)、`dedupe`(默认开)、`use_llm`(默认 0)、`model``pool_*`。回归数据:testdata/ocr_srt_run_ac7f480a3ccb.srt(真实任务 1666 条 OCR 输出) |
| `srt-to-dual-eye-ass` | `cn_srt_uri` | `ass_uri` | 参数:`resolution`(如 `3840x1920`)、`margin_top`(顶部安全边距,**默认 700**——2026-09 调整:120 落在画面最顶需抬头看,700 使字幕处于视线自然可读位置)。左右眼各占左右半幅且水平相对位置一致(**A-1 零视差**:字幕固定在屏幕平面,不做景深偏移);对齐 `an8` 顶部居中 + `MarginV=margin_top`(**B-1 顶部安全区**,避开画面中央人脸区);文字填充 `&HB3FFFFFF`(约 70% 透明)描边 `&H80000000`(半透明黑),降低遮挡感 |
@@ -272,7 +286,7 @@ subtitle-ocr 逐帧调 vlm-ocr 时使用 `nodes/adaptive_pool.py` 的自适应
| `WHISPER_DEVICE` | `auto` | 转写设备 |
| `LLM_API_BASE` | `https://api.siliconflow.cn/v1/chat/completions` | LLM 兼容接口 |
| `LLM_API_KEY` | 空(读 `.env` | SiliconFlow Bearer Key,存于 gitignored 的 `.env` |
| `LLM_MODEL` | `Qwen/Qwen3.6-35B-A3B` | LLM 模型名 |
| `LLM_MODEL` | `Qwen/Qwen3.5-35B-A3B` | LLM 模型名2026-09 由 `Qwen/Qwen3.6-35B-A3B` 切换:同片 A/B 全量评测质量持平、0.232 s/行(基线档最快),见 `data/experiments/translate_models/REPORT.md`)。**例外**`subtitle-correction` 节点**有意**保留旧兜底 `Qwen/Qwen3.6-35B-A3B`——该节点错听泛化实测新模型 0/4、旧模型 4/4(复现:同一误听场景各跑 4 次),参见 `tests/test_llm_default_model.py` |
| `LLM_TIMEOUT_SECONDS` | `600` | LLM 单请求超时 |
| `OLLAMA_HOST` | `http://192.168.123.70:11434` | Ollama 服务地址 |
| `VLM_MODEL` | `glm-ocr:latest` | VLM OCR 模型 |
+10 -1
View File
@@ -15,9 +15,18 @@ uv run uvicorn wov_app.main:app --reload
- 模型权重本地优先:把 whisper 模型放在 `model/faster-whisper-large-v3`
(含 `model.bin`)即可完全离线运行。
- LLM 翻译默认使用 SiliconFlow`Qwen/Qwen3.6-35B-A3B`),API Key 放在
- LLM 翻译默认使用 SiliconFlow`Qwen/Qwen3.5-35B-A3B`),API Key 放在
gitignored 的 `.env``LLM_API_KEY=sk-...`),启动时自动加载;也可用
`LLM_API_BASE` / `LLM_MODEL` 环境变量覆盖。
- 默认模型切换为 `Qwen/Qwen3.5-35B-A3B`(2026-09):对同片 2 小时日语 ASR 做
8 个模型全量对比,该模型质量与旧默认 `Qwen/Qwen3.6-35B-A3B` 持平,
速度 0.232 s/行(全评测最快),评测报告见
`data/experiments/translate_models/REPORT.md`
- `subtitle-correction` 节点**不跟随**该默认值,有意保留
`Qwen/Qwen3.6-35B-A3B`:错听泛化实测新模型 0/4、旧模型 4/4。
- `llm-filter` 节点的 LLM 五类分类层**默认关闭**(`use_llm=0`):
实测该层额外删除的 131 条中 56% 是真实对话,净收益为负;
现只跑确定性规则层,误删真对话从 73 条降为 0 条且无 LLM 调用。
## 目录
+1 -1
View File
@@ -16,7 +16,7 @@ load_dotenv(Path(".env"))
API_BASE = os.getenv("LLM_API_BASE", "https://api.siliconflow.cn/v1/chat/completions")
API_KEY = os.getenv("LLM_API_KEY", "")
MODEL = os.getenv("LLM_MODEL", "Qwen/Qwen3.6-35B-A3B")
MODEL = os.getenv("LLM_MODEL", "Qwen/Qwen3.5-35B-A3B")
def one_call(idx: int) -> str: