# 设计决策与踩坑记录 本文件记录"为什么这样做":模型/参数选择的实测依据、历史事故与踩坑。 **当前生效的规则**以对应专题文档为准(本文件只解释理由): - 节点参数与默认值:[node-protocol.md](./node-protocol.md) - 工作流与模型配置:[workflows.md](./workflows.md) / [configuration.md](./configuration.md) - 代码缺陷与修复过程:[代码审查问题跟踪.md](./代码审查问题跟踪.md) ## 帧文件必须按帧号数值排序(14236 帧事故) - **现象**:run_339ec7ee437f 的 14236 帧任务中,字幕时间与图像错位。 - **根因**:ffmpeg `%04d` 编号超过 9999 帧后扩为 5 位,字典序 `sorted()` 会把 5 位编号排在 4 位之前。 - **结论**:帧文件必须按帧号数值排序读取(`_sorted_frame_files`), 回归测试见 `test_frame_files_read_order_matches_frame_number`。 ## glm-ocr 重复循环问题与源头修复 - **根因**:glm-ocr 生成阶段存在已知 bug(M-RoPE delta 未传递,大图触发 重复循环;GitHub #454 / #16892)。`keep_alive` 与其无关(实测无效)。 - **修复路径**(现行防护措施见 [node-protocol.md](./node-protocol.md#glm-ocr-调用与重复循环防护)): 1. `frame-extract` 裁切后把帧压缩到 720p 内——过大输入图是触发条件之一。 2. `vlm` 请求体加 `repeat_penalty` + `num_predict` 压制重复。 3. `subtitle-ocr` 用 `max_result_chars` 把超长输出判为异常并跳过该帧。 ## llm-filter 的 LLM 分类层默认关闭 - **决策(2026-09)**:`llm-filter` 只跑确定性规则层,LLM 五类分类层默认关闭 (`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` 断点存档;按文本去重。 - **复现**:`scripts/regenerate_filter_ac7f480a3ccb.py`, 回归数据 `tests/nodes/test_llm_filter/data/ocr_srt_run_ac7f480a3ccb.srt`。 ## 翻译模型默认值切换与 subtitle-correction 例外 - **默认切换(2026-09)**:`LLM_MODEL` 由 `Qwen/Qwen3.6-35B-A3B` 改为 `Qwen/Qwen3.5-35B-A3B`——同片 2 小时日语 ASR 全量对比,质量持平、 0.232 s/行(评测中最快)。 - **例外**:`subtitle-correction` 节点**有意**保留旧兜底 `Qwen/Qwen3.6-35B-A3B`——该节点错听泛化实测新模型 0/4、旧模型 4/4 (复现:同一误听场景各跑 4 次),见 `tests/test_llm_default_model.py`。 - **评测数据**:`data/experiments/translate_models/REPORT.md`, 工具链 `scripts/bench_translate_models.py` 等。 ## whisper large-v2 vs large-v3 切换 - **决策(2026-09)**:通用转写模型从 large-v3 切到 `faster-whisper-large-v2`。 - **依据**:savr-1054 全片 A/B 实测,无 VAD 幻觉长段归零、开头漏句救回, VAD 链路条数与覆盖小幅领先。 - **数据**:`data/experiments/whisper_v2_vs_v3/`, 脚本 `scripts/compare_whisper_v2_vs_v3.py`, 调研记录 [调研-whisper漏句与decode_full验证.md](./调研-whisper漏句与decode_full验证.md)。 ## VR 字幕景深与遮挡方案选型 - **结论**:采用 **A-1 零视差**(字幕固定在屏幕平面,不做景深偏移)+ **B-1 顶部安全区**(`an8` 顶部居中,`margin_top` 默认 700), 并用半透明填充 + 半透明描边降低遮挡感。 - **调研全文**:[VR双目字幕景深与遮挡调查报告.md](./VR双目字幕景深与遮挡调查报告.md)。 ## 批量任务先写明细再排队(CREATING 状态) - **现象**:新建批量任务后偶发任务被标 COMPLETED、`done=0/N`,剩余视频永远不再 被处理(例:`batch_ac585c5458de` 唯一明细还 PENDING 而任务已 COMPLETED)。 - **根因**:`create_job` 先把任务行以 QUEUED 入库(此时引擎就看得见),再逐条登记 明细(扫描媒体库时 500+ 条要数秒);引擎轮询到的快照可能没包含剩余明细,收尾时 “无未结束明细”检查也看不到它们,于是把任务标 COMPLETED。 - **结论**:任务行改以 `CREATING` 入库,明细全部登记完才置 QUEUED(引擎只取 QUEUED);登记中途异常置 FAILED 并抛给路由;启动时把上一进程遗留的 CREATING 统一置 FAILED(`fail_creating_batch_jobs`),避免静默残留。 - **旧数据修复**:`scripts/fix_zombie_batch_jobs.py` 仍用于处理历史 “COMPLETED 但仍有未结束明细”的脏数据。回归测试见 `test_batch.py` 的 `test_job_hidden_from_engine_until_details_written`。 ## 批量流水线按 GPU 资源调度(而不是按"是否云端"分支) - **现象**:LLM 走线上端点时翻译阶段不占显存,但组内阶段仍是"先全部提音、再全部 转写、再全部翻译"的串行推进,翻译期间 GPU 全程空闲(实测 19.6W / 272MiB, 占整轮挂钟约 40%)。 - **结论**:阶段能否启动只看资源。`wov_app.resources.stage_gpu_need_mb` 给出每个 阶段的显存需求(whisper 按权重 ×1.45、本机 LLM 端点按预留、线上端点为 0), 进程内 `GpuGate` 负责准入:不需要 GPU 的阶段立刻放行(转写与线上翻译并行); 需要 GPU 的阶段互斥,并按"阶段索引最小者优先"派发,组内因此仍是先跑完全部转写 再进翻译——本机 Ollama 模型每组只加载一次,行为与旧实现一致。 - **为什么不是"配置分支"**:代码不判断端点是不是云端,只读"这个阶段要不要占 GPU、现在够不够"。同一份代码在本机模型下自动退化为串行、在线上模型下自动并行。 - **降级**:探测不到 `nvidia-smi` 时退化为 GPU 阶段互斥(与串行结果一致); 局域网地址上的本机 Ollama 需用 `WOV_LOCAL_MODEL_HOSTS` 声明,否则会被当成远端。 - **并发写**:流水线多线程写产物/进度,SQLite 开 WAL + busy timeout (`WOV_DB_BUSY_TIMEOUT_SECONDS`)。 ## 相关文档 - 当前生效的参数与协议:[node-protocol.md](./node-protocol.md) - 运维语义与批量流程:[operations.md](./operations.md) - 缺陷 ID 与验收标准:[代码审查问题跟踪.md](./代码审查问题跟踪.md)