Files
vrsub/AGENTS.md
T
cat-shark 52512c26e5 fix: 批量任务进度实时汇总,暂停/中断时前端不再显示 0/总数 0%
batch_jobs.done 此前只在任务整体完成时一次性汇总,处理中途(尤其被暂停)
恒为 0,导致前端显示 "0/431 完成 0%" 而实际已处理多个视频
(回归 batch_fee668175444:已处理 15 个仍显示 0/431)。

- db: 新增 sync_batch_job_progress(按明细实时重算 done=COMPLETED+SKIPPED、
  failed=FAILED 并落库)与 refresh_batch_job(对齐后返回最新任务)
- batch 引擎: 暂停返回、SKIPPED/COMPLETED continue、视频缺失、单视频异常后、
  任务收尾等边界统一调用实时对齐,替代收尾一次性 sum
- routers/batch: 列表/详情读取前实时对齐,即使引擎不在运行也返回真实进度
- tests: 新增 6 条 TDD 回归测试覆盖 db 助手、引擎暂停、SKIPPED、API 读取侧

同时按用户要求:移除 100% 行覆盖率强制门槛(pytest 不再 --cov-fail-under),
约定小改动只跑相关测试、保证功能可用即可(AGENTS.md 与 pyproject.toml)。
2026-09-06 16:57:05 +08:00

33 KiB
Raw Blame History

VRSub(单体版)

本仓库是"为视频生成 VR 双眼字幕"的单体应用(WOV AI Workflow Platform 的 单机实现):FastAPI 后端、工作流调度器与全部节点(提音 / 转写 / 翻译 / ASS)在同一个进程内运行,不再启动子进程、不再走节点 HTTP 协议。 由原分布式多仓库(wov-api / wov-web / wov-sdk / wov-node-*)合并而来, 本 AGENTS.md 汇总了各仓库的约定与规范。

提交规则(强制,2026-08

  • 未经用户明确许可,禁止执行 git commit / git push,包括"小步提交"。
  • 完成改动后只汇报改动内容与验证结果,等待用户指示;用户明确说出 "提交 / push / 推上去" 等指令时才可执行提交。
  • 需要用户确认的场景:新增功能、修复、文档改动、工作流/清单数据改动等 一切涉及 git 写操作的行为。

架构概览

vrsub/
├── src/wov_sdk/       # 协议数据模型(NodeManifest/InvokeRequest/InvokeResponse/
│                      #   WorkflowDefinition 等),与分布式版保持一致
├── src/wov_app/       # 应用层:main/config/db/registry/scheduler/batch/seed/routers
│   └── routers/       #   apps.py(用户端)、workflows.py(管理端)、batch.py(批量处理)
├── nodes/             # 进程内节点实现:echo/ffmpeg/whisper/llm/ass
├── manifests/         # 各节点清单 JSONecho.json/ffmpeg.json/...
├── workflows/         # 默认工作流定义 JSON(模型/链路均为数据,改模型不改代码)
├── web/               # 静态前端(index/tasks/admin/workflow + assets
├── model/             # 本地 whisper 权重(gitignored
├── data/              # SQLite + 上传/产物存储(gitignored
└── tests/             # 单元/API/冒烟测试(覆盖核心路径,不强制 100%)

核心机制

  • 节点注册表src/wov_app/registry.py):启动时把 manifests/*.jsonnodes/*.pyinvoke 处理器静态注册到进程内字典,调度器按 node_type 直接调用。协议数据模型不变,为将来回退分布式保留兼容桥梁。
  • 调度器src/wov_app/scheduler.py):后台线程轮询 SQLite 中的 QUEUED 任务,按工作流 DAG 拓扑顺序调用节点,产物按 data/storage/runs/<run_id>/steps/<node_id>/ 落盘并登记到 artifacts 表。
  • 前端:由 FastAPI 静态挂载 web/,节点注册/实例管理页面已移除, 仅保留应用中心、任务管理、批量处理、管理后台(工作流)与工作流编排。
  • 工作流编排页(web/workflow.html:支持新建工作流(空表单预填演示模板), 从列表"编辑"加载任一工作流的最新定义(ID 锁定,保存即追加新版本);"版本" 查看全部历史版本并可"加载到编辑器"(对比/回滚后另存新版本);管理后台 (admin.html)无编辑器,点"编辑"自动跳转 workflow.html?edit=<id> 加载。

节点输入/输出协议

节点统一签名 invoke(request: InvokeRequest) -> InvokeResponse,通过产物 URI 交换数据(节点之间不直接调用,不共享内存状态)。

节点 IDnode_type 输入 输出 说明
echo text / file_uri textfile_uri 示例节点,验证协议链路
ffmpeg-extract video_uri audio_uriWAV 参数:sample_ratechannels
faster-whisper audio_uri16kHz 单声道) srt_uri 参数:languagetaskmodel_pathdevicecompute_typebeam_sizevad_filter(默认开)、condition_on_previous_textchunk_seconds
llm-translate srt_uri cn_srt_uri 参数:target_languagemodel
vlm-ocr image_uri texttext_uri 直接调本地 Ollama 多模态模型(glm-ocr)的 /api/chat 做视频帧 OCR(流式 + 5s 上限),参数:modelollama_hostprompttimeout_secondskeep_alivenum_predicttemperaturerepeat_penalty
frame-extract video_uri frames_manifestframe_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_uricount 自适应线程池并发逐帧调 vlm-ocr → 垃圾过滤(无文字帧)→ 相同字幕合并(记录最后可见帧)→ 组装 SRT,消失时间=最后可见帧+采样间隔(间隔从帧清单推导),参数:min_charsmin_alnum_ratiogarbage_tokenspool_min_workers/pool_max_workers/pool_window_seconds/pool_fast_threshold/pool_slow_threshold
llm-filter srt_uri srt_urikeptremoved 两级过滤:①规则层(不调 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_sizemin_keep_lenoverlay_tokensJSON 数组)、dedupe(默认开)、modelpool_min_workers/pool_max_workers/pool_window_seconds/pool_fast_threshold/pool_slow_threshold。回归数据:testdata/ocr_srt_run_ac7f480a3ccb.srt(真实任务 1666 条 OCR 输出)
srt-to-dual-eye-ass cn_srt_uri ass_uri 参数:resolution(如 3840x1920)、margin_top(顶部安全边距,默认 120)。左右眼各占左右半幅且水平相对位置一致(A-1 零视差:字幕固定在屏幕平面,不做景深偏移);对齐 an8 顶部居中 + MarginV=margin_topB-1 顶部安全区,避开画面中央人脸区,2026-09);文字填充 &HB3FFFFFF(约 70% 透明)描边 &H80000000(半透明黑),降低遮挡感

模型权重解析(本地优先)

whisper 节点按以下顺序解析模型路径,默认避免从远端下载:

  1. 请求参数 model_path;裸模型名(不含路径分隔符)会在 model/<名称> 下解析。
  2. 环境变量 WHISPER_MODEL_PATH
  3. 本地候选目录(存在且含 model.bin 即使用):
    • 单体根目录 model/faster-whisper-large-v3
    • nodes/model/faster-whisper-large-v3
  4. 兜底:large-v3(需要联网从 Hugging Face 下载)。

把权重放在 model/ 目录即可完全离线运行。当前已下载模型:

  • model/faster-whisper-large-v3:通用转写模型(demo 工作流)。
  • model/whisper-large-v2-translate-zh-v0.2-st-ct2:中文直出模型 chickenrice0721/whisper-large-v2-translate-zh-v0.2-st-ct2),配合 task=translate 直接生成中文,无需 LLM 翻译(zh-direct 工作流)。
  • model/whisper-large-v3-translate-zh-v0.1-lt-ct2:早期中文直出模型, 已无工作流引用,保留在盘上待处理。

内置工作流

ID 名称 链路 说明
demo 视频字幕生成 提音 → 转写 → LLM 翻译 → ASS 通用链路,翻译走 SiliconFlow
zh-direct 中文直出字幕 提音 → 中文转写 → ASS 中文直出模型,无 LLM 步骤
ocr-subtitle 字幕OCR提取 抽帧 → 逐帧 OCR → 汇总 SRT → LLM 过滤 提取烧录字幕做基准数据;前端框选 crop;LLM 过滤多余/无意义字幕

最终产物按 上传文件名.标识.时间戳 重命名(如 test01.zh-CN.20260815123000.srt), 标识优先取节点的 target_language 参数,否则用产物别名。

任务参数覆盖(前端框选)

创建任务时可携带可选 params 表单字段(JSON):{"节点ID": {"参数": 值}} 随任务持久化(param_overrides),调度执行时合并进对应节点参数。字幕 OCR 前端把框选的 crop 按此传给 frame-extract 节点。

切换模型不改代码

  • 模型是工作流 DAG 中 asr 节点的 model_path 参数(数据),两个内置工作流 均已显式声明:demo 用 faster-whisper-large-v3zh-direct 用中文直出模型。
  • 切换模型 = 改 workflows/*.json 或管理页面 DAG JSON → 保存新版本 → 发布, 全程不涉及代码;新库启动时从 JSON 重新 seed。
  • 默认工作流定义存放在 workflows/*.json(数据文件),代码只负责加载。

长音频处理

当前策略:分块转写,默认每 1 分钟一块chunk_seconds=602026-08 调整)。 whisper 节点内部用 ffmpeg 把音频切成块 → 逐块转写 → 按偏移合并为完整 SRT:

  • 内存/显存有界(模型 + 单块音频),任意时长可处理,失败粒度小。
  • 分块是应用层工程策略,与模型训练格式无关:whisper 训练/推理都按 30s 窗口解码,任意块大小均适用。
  • 每块 offset = 块序号 × chunk_seconds,SRT 序号连续;切块失败自动回退 整段单次转写。
  • chunk_seconds=0 可关闭分块;大小按工作流 DAG 参数(数据)调整。
  • 同时默认 condition_on_previous_text=false(每块/每窗口独立解码,防重复)。
  • vad_filter 默认开启(2026-08 用户决定):过滤静音段提速并减少无语音处 幻觉。注意 VAD 靠压缩时间轴回映射(SpeechTimestampsMap),长静音场景曾实测 错位(30s 静音致第二段语音从 ~40s 落到 10s);如需极致对齐可显式传 vad_filter=false
  • 分块偏移按每块实际时长累积(WAV 头精确):ffmpeg 切出的块实际时长不等于 块长(如 60.05s),用 块序号×块长 的假设值会随块数累积漂移;改为按真实 时长累加后,字幕时间轴与原始音频严格一致。
  • 参考:openai/whisper 重复问题(issue #1026/#1046PR #1052/#1253)、 SYSTRAN/faster-whisper issue #465。

glm-ocr 重复循环问题与源头修复(2026-08)

  • 根因:glm-ocr 生成阶段存在已知 bugM-RoPE delta 未传递,大图触发 重复循环;GitHub #454 / #16892)。keep_alive 与其无关(实测无效)。
  • 源头修复
    1. frame-extract 裁切后把帧压缩到 720p 内(仅缩小,保持宽高比)—— 过大输入图是触发条件之一。
    2. vlm 请求体 options.repeat_penalty(默认 1.2+ num_predict (默认 256)压制重复。
    3. subtitle-ocr 增加 max_result_chars(默认 200):模型输出超长视为 异常(重复循环等),直接报错并跳过该帧
  • glm-ocr 调用结构:走 Ollama /api/chat,识别指令放系统提示词, 用户消息只携带图片(content 为空、images 传 base64),stream=True 逐行接收,stop: ["\n", "\n答", "答"] 命中即停止(输出首个换行即停 + 阻止“答:”式重复循环),temperature 默认 0.3、repeat_penalty 默认 1、num_predict(默认 256)随请求透传。
  • gettext 标签防御性提取2026-08):若模型输出含 <gettext></gettext> 标签(旧提示词要求)则取第一个标签内文本,多个标签取第一个防重复循环; 未按格式输出时回退原文。当前默认提示词为"提取图像中的文字,不要描述 图片中的内容"(字幕流水线在 ocr-subtitle 工作流的 subtitle-ocr 节点参数 中显式指定,经 subtitle-ocr 透传给 vlm-ocr)。
  • 每次调用 5 秒上限2026-08 调整):vlm 请求 stream=True 逐行读取, 每次调用整体受 5 秒截止时间约束(timeout_seconds 参数 / VLM_TIMEOUT_SECONDS,默认 5),超过即终止返回 failed,不再等待后续 流式块。
  • 不做文本加工:除协议要求的 gettext 标签提取外,不再对模型输出做 过滤/去重等文本加工,结果原样使用,仅受长度上限约束。

VLM OCR 自适应并发(2026-08

subtitle-ocr 逐帧调 vlm-ocr 时使用 nodes/adaptive_pool.py 的自适应线程池 弹性并发:

  • pool_min_workers(默认 1)起步,按滚动窗口pool_window_seconds, 默认 10s)统计已完成任务的平均响应时间;
  • 平均响应 < pool_fast_threshold(默认 0.3s)→ 线程数 +1(上限 pool_max_workers,默认 16)——服务端空闲就加大并发加速处理;
  • 平均响应 > pool_slow_threshold(默认 1.0s)→ 线程数 -1(下限 1)—— 服务端变慢就退避,避免盲目并发压垮本地 Ollama;
  • 结果按帧顺序返回,SRT 时间轴不受并发影响;worker 需无共享可变状态 (vlm-ocr 处理器为纯函数,线程安全)。

前端 OCR 框选

首页选择工作流后,若 DAG 中存在声明 crop 参数的节点(frame-extract), 自动切换到框选面板:视频预览 + 拖动框选字幕区域 → 生成 crop 比例 → 框选完成后才可提交(未框选时提交按钮禁用)。矩形↔crop 换算为纯函数 (web/assets/crop.js,含 letterbox 处理),由 node 单测覆盖。

环境变量

变量 默认值 说明
WOV_DATA_DIR <根>/data 数据目录
WOV_DB_PATH <根>/data/wov.db SQLite 路径
WOV_STORAGE_DIR <根>/data/storage 上传与产物根目录
WOV_AUTO_SEED 1 启动时创建 demo 工作流
WOV_SCHEDULER_ENABLED 1 启动后台调度器
WOV_SCHEDULER_INTERVAL_SECONDS 1.0 调度轮询间隔
WOV_CLEANUP_ENABLED 1 开启孤儿数据定时清理
WOV_CLEANUP_INTERVAL_SECONDS 3600 孤儿清理扫描周期(秒)
WOV_CLEANUP_GRACE_SECONDS 3600 孤儿清理宽限期(秒)
WOV_BATCH_ENABLED 1 开启文件夹批量处理引擎(处理 source=batch 任务)
WOV_BATCH_INTERVAL_SECONDS 1.0 批量引擎轮询间隔
WHISPER_MODEL_PATH 见上 显式指定 whisper 模型路径
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_TIMEOUT_SECONDS 600 LLM 单请求超时
OLLAMA_HOST http://192.168.123.70:11434 Ollama 服务地址
VLM_MODEL glm-ocr:latest VLM OCR 模型
VLM_PROMPT 提取图像中的文字,不要描述图片中的内容 OCR 提示词(字幕流水线在 ocr-subtitle 工作流的 subtitle-ocr 节点参数中显式指定同一提示词)
VLM_TIMEOUT_SECONDS 5 VLM 单请求整体超时上限(每次调用 5 秒,超时即终止;流式读取同样受此截止约束)
FFMPEG_BIN 显式 ffmpeg 路径(否则 PATH → imageio-ffmpeg

启动

uv sync
uv run uvicorn wov_app.main:app --reload

访问:

http://127.0.0.1:8000/           应用中心(上传视频 → 字幕生成)
http://127.0.0.1:8000/tasks.html 任务管理
http://127.0.0.1:8000/admin.html 管理后台(工作流)
http://127.0.0.1:8000/workflow.html 工作流编排(DAG JSON
http://127.0.0.1:8000/docs      API 文档

Python 环境与 uv 管理

  • 统一使用 uv 管理虚拟环境和依赖,禁止直接使用 pip 修改依赖。
  • 基础命令:uv sync(安装含 dev 组依赖)、uv run <command>uv add <package>uv lock
  • 虚拟环境位于 .venv,测试依赖在 [dependency-groups] dev
  • 新增依赖时使用 uv add,不修改系统 Python 或全局环境。

孤儿数据清理

应用内置后台清理器(src/wov_app/maintenance.py),按周期自动清理死数据:

  • 自动删除:无任务记录的上传/步骤残留目录;COMPLETED 且产物文件全部丢失、 超过宽限期(默认 1 小时)的任务记录(下载已全部 404)。
  • 绝不自动删除FAILED 任务(可重试)、QUEUED/RUNNING 任务、宽限期内的任务、 仍有产物文件的任务。
  • 手动删除任务仅通过删除接口(DELETE /api/runs/{run_id})或管理界面进行。

任务暂停/继续(2026-08

  • 状态机QUEUED / RUNNING / PAUSED / COMPLETED / FAILED。排队中或运行中的 任务可暂停(POST /api/runs/{run_id}/pause),PAUSED 可继续 POST /api/runs/{run_id}/resume → 恢复 QUEUED)。
  • 调度器语义next_queued_run 只取 QUEUED——PAUSED 任务不会被调度器 自动拾起(修复回归:此前 PAUSED 被拾起后 execute_run 先置 RUNNING 再检查, 节点循环读到的是刚改的 RUNNING,"暂停检查"永远不成立 → 任务被复活继续跑, 表现为"点击暂停反而开始任务")。execute_run 以 PAUSED 进入时直接返回保持暂停, 必须用户显式 resumePAUSED → QUEUED)后才真正执行;运行中被暂停的任务在每个 节点边界检查状态停下保持 PAUSED(当前节点执行完后才停);继续时从产物表 (restore_run_outputs,剥去"节点ID."前缀还原输出名)重建已完成节点的输出, 跳过已完成节点断点续跑,最后补做 final_outputs 收尾。
  • 重启恢复:进程被杀/重启时遗留的 RUNNING 任务在启动时被 recover_interrupted_runs 恢复为 QUEUED(保留产物),调度器自动断点续跑; PAUSED 任务保持不变,等待显式 resume。
  • 节点级断点(subtitle-ocrOCR 每帧完成后立即把 {frame, text} 追加到 steps/ocr/ocr_partial.jsonl(多线程下加锁串行化)。invoke 启动时读取存档, 只对未处理帧调用 vlm-ocr,存档文本与新增结果合并后组装 SRT——2 小时视频级 OCR 任务中断/重启后不重复已处理帧,产物与一次跑完逐字节一致。
  • 节点内暂停响应(subtitle-ocr:暂停接口(POST /api/runs/{run_id}/pause 除置 PAUSED 外还向 run 根目录写入 paused.flagOCR 工作线程逐帧检查该 信号,存在即立即中止(不 OCR、不入存档,恢复时重跑该帧),invoke 返回 failed;调度器捕获节点异常时若任务已是 PAUSED 则保持 PAUSED 不标 FAILED, resume 时清除信号并从断点存档继续——点击暂停后 OCR 秒级停下,不再等整个 节点跑完。继续/重试接口与调度器执行前都会清理残留信号。
  • 前端:任务管理页为 QUEUED/RUNNING 提供"暂停"、PAUSED 提供"继续"按钮。
  • 进度日志(数据处理速度)
    • 调度器:每节点完成打印"任务 X 进度 i/N 节点: Y 耗时 Zs, 运行累计 Ws"
    • subtitle-ocrOCR 进度 X/Y 帧 (Z 帧/s, 平均 Ws/帧, 线程 N/M)llm-filter 字幕判定进度 X/Y 条 (Z 条/s, 平均 Ws/条, 线程 N/M)——W 为最近窗口 平均单任务耗时(窗口未满时回退累计平均),N 为当前目标线程数、Mpool_max_workers 上限,用于判断多线程是否因单次处理过慢(窗口平均 ≥ pool_fast_threshold)而未扩容; (线程池 on_progress 回调,每任务完成触发);
    • whisper:分块转写打印"分块 X/Y 完成 offset=... 耗时 Zs (Nx 实时, 累计 ...s)"
    • frame-extractffmpeg -progress 输出解析 frame=N,打印"抽帧进度 X/Y 帧 (Z 帧/s)"。

文件夹批量处理(2026-09 更新)

本地版核心能力:不把视频上传到工作目录,直接读取用户所选文件夹下的全部 视频,逐个执行所选流水线。入口为批量处理页(web/batch.html,导航"批量处理"), 后端为 src/wov_app/batch.pyBatchWorker(单线程轮询线程,处理 source=batch 的运行,与主调度器互不抢占)与 routers/batch.py

  • 路径选择:批量页点击"选择文件夹…"按钮弹出目录树选择器(懒加载),选完 回填只读路径框。浏览器拿不到所选文件夹的绝对路径,因此由本地后端提供目录 浏览:GET /api/batch/rootsWindows 盘符 / POSIX 根 + 家目录)、 GET /api/batch/dirs?path=(列直接子目录,隐藏目录过滤;不存在/不可读返回 空列表不报 500)。只暴露目录名,不返回文件内容。
  • 创建任务时一次性定位(2026-09 起)POST /api/batch/jobs {folder, workflow_id, recursive} 只扫描一次文件夹并把每个视频登记为 batch_videos 明细(PENDING/RUNNING/PAUSED/COMPLETED/FAILED/SKIPPED)。视频所在目录 (视频旁)若已存在文件名含视频名的字幕文件.srt/.ass/.ssa/.vtt list_sidecar_subtitles 判定,如 movie.CN.srtmovie.CN_dual_eye.ass), 说明该视频已有字幕,创建即记 SKIPPED——不为它触发任何流水线。运行时 BatchWorker 只消费这批已定位的明细,不再重新扫描文件夹(运行期间新增/ 删除的视频不会改变本次任务的范围)。校验失败(文件夹不存在/未发布工作流/ 无版本/一个视频都没有)返回 422。
  • 产物放在视频旁:每个视频处理完成后,把工作流 final_outputs 对应的最终 产物文件(字幕流水线即中文 .srt 与双目 .ass复制一份到视频所在目录 与 .mp4 放在一起(_place_products)。文件名对齐媒体库既有约定:中文字幕 存为 <视频名>.CN.srt、双目字幕存为 <视频名>.CN_dual_eye.ass(稳定无时间戳, _sidecar_product_name 映射,其余扩展名产物保留原文件名;同名目标直接覆盖)。 文件名含视频主名,下次批量扫描会命中"已有字幕"规则直接跳过该视频。
  • 过程文件清理(2026-09 起):视频收尾完成后删除该视频的整个工作空间与 run 记录(音频/分块/帧图/节点产物不留残),防止媒体库把切片数据当视频入库。 工作空间位于应用私有目录 data/storage/batch/<job_id>/<bv_id>/ (不再放视频同名文件夹),与用户视频库天然隔离;暂停/失败的视频保留工作空间 以便断点续跑。per-video 的 WorkflowScheduler 实例以该目录为 storage—— 完整复用 DAG 拓扑执行、产物表登记与断点续跑逻辑。
  • 暂停/继续POST /api/batch/jobs/{id}/pause 把任务置 PAUSED 并暂停当前 run(写 paused.flagwhisper 分块间检查、OCR 逐帧检查后中止,当前节点 执行完才停);resume 恢复 QUEUED,引擎从断点继续——PAUSED 视频的 run 显式 resume 后从产物表续跑,未开始的视频接着处理。重启进程后 RUNNING 残留 run 由 recover_interrupted_runs 恢复,暂停的继续处理。
  • 失败容错:单个视频失败(节点失败/文件缺失)记为 FAILED,批量任务继续 处理后续视频,结束后统计 done/failed;DAG 解析/任务级异常把任务置 FAILED。 重跑保留产物2026-08,修复 run_e2b74e89e232 实测):FAILED 视频重新处理 时不再 reset_run 清空产物记录,而是保留已完成节点的 artifacts 恢复 QUEUED execute_run 从产物表跳过已完成节点、只重跑失败节点——extract/ocr 等长耗时 成果不浪费;配合 llm-filter/OCR 的节点级断点存档,失败节点自身也只重判未完成 条目。前端对"部分失败"COMPLETED 且 failed>0)用红色徽章醒目标示。
  • 产物下载GET /api/batch/jobs/{id}/videos/{vid}/download?alias=<文件名> 解析并返回视频旁的字幕文件;旧版 batch.done.json 完成标记里的语义别名 (位于旧 work_dir)仍兼容可下载。详情/创建响应里每个视频的 finals 合并上述 两处来源。
  • 孤儿清理保护source=batch 的运行跳过自动清理——其 run 位于私有 storage/batch/... 下,普通孤儿逻辑会误判删除,且 _remove_run 还会删除 input_uri 的父目录(用户的整个视频文件夹)。
  • 删除任务DELETE /api/batch/jobs/{id} 清理数据库记录(含关联 run)与 应用私有工作空间残留;视频旁已放置的产物属于用户数据,保留不删。
  • 环境变量WOV_BATCH_ENABLED(默认 1)、WOV_BATCH_INTERVAL_SECONDS (默认 1.0)。

测试

  • 测试以保证功能可用为目标,不强制 100% 行覆盖率pytest 已移除 --cov-fail-under=100 门槛);需要查看覆盖率时可手动追加 uv run pytest --cov=src --cov=nodes
  • 小改动只跑相关测试,避免每次都完整跑全量测试浪费时间;改动涉及 哪个模块就跑对应测试文件(如 uv run pytest tests/test_batch.py), 确认相关用例通过、功能可用即可。完整跑全量测试只在改动影响面大时进行。
  • 测试必须调用真实代码路径,不得在测试类中重写业务逻辑来模拟被测功能。
  • 测试必须使用真实数据:真实音频(合法 WAV/PCM)、真实 JSON/数据库/文件; 禁止用占位字节(如 b"x")或伪造结构冒充被测数据——假数据测试只能凑覆盖率, 无法验证真实行为,视为无意义测试。
  • 只允许在 I/O 边界使用 mock/stub:文件系统、网络、子进程、环境变量、时间、 模型推理(重模型不进入单元测试;注入的假模型必须返回结构真实的分段, 且必须配套真实模型集成测试,见下)。
  • 真实模型集成测试:使用真实 faster-whisper 模型 + 真实音频素材验证 端到端转写(tests/test_integration_whisper.py);本地无模型或素材时跳过, 有则必须执行,作为对假模型单测的校准。
  • 测试资产存放 testdata/:图片(ocr_text.png)、语音(speech_60s.wav) 等测试媒体一次性生成后入库,测试直接复用,禁止在测试执行时再生成; 缺失时测试跳过而非现场生成。大体积视频素材放 data/testdata/gitignored)。OCR 相关资产: ocr_text.png(有文字)、ocr_notext.png(无文字帧)、subtitle_10s.mp4 (烧录 SUB 001@1-4s / SUB 002@6-9s 的 10s 测试视频)、test_real_hav_sub.png (真实视频字幕截图,VLM 集成测试期望识别出"还有没有什么困扰 或者奇怪的地方吗")、 ocr_srt_run_ac7f480a3ccb.srt(真实任务 1666 条 OCR 输出,llm-filter 回归)、 frames_manifest_full.json + ocr_frames_full.json(真实任务 run_ac7f480a3ccb 全部 14236 帧的帧清单与逐帧 OCR 文本,多线程顺序测试常驻夹具;配合 ocr_srt_run_ac7f480a3ccb.srt 作为单线程确认基线,见 tests/test_subtitle_ocr_order_threading.py)。
  • 开发流程强制 TDD(红-绿-重构):任何新功能/修复必须先写失败测试(红), 再实现最小代码让其通过(绿),最后重构保持整洁;不允许先写实现后补测试。
  • 测试运行:uv run pytest;全部测试位于 tests/
  • 测试(无论是否测覆盖率)只保证代码路径被执行,不覆盖端口占用、防火墙、 权限等外部环境状态;端口问题用启动检查、端口检查与 uvicorn 冒烟测试补充。
  • 本地出现 WinError 10013 / WinError 10048 时,先用 netstat -ano | findstr :<port> 确认是否有残留监听进程。

代码注释规范

  • 本仓库所有源码(Python、JavaScript、HTML、CSS、TOML 等支持注释的文件) 必须配有详细中文注释,说明模块/文件职责、核心类与函数的作用以及关键逻辑, 确保后续维护人员无需通读全部实现即可快速理解工作原理。
  • 新增或修改代码时,必须同步补充或更新对应注释;不得删除已有注释。
  • 测试代码同样必须配有中文注释,说明每条测试验证的行为与覆盖的路径。
  • JSON 数据文件(manifests/*.json)按 JSON 规范不支持注释,字段语义以 src/wov_sdk/models.pyNodeManifest 模型注释和本文档为准; 修改 JSON 字段时须同步更新文档。

目标运行环境

  • 本服务的最终部署目标是 Linux,通常以 Docker/Kubernetes 容器运行。
  • 当前 Windows 只作为本地开发环境,不允许在业务代码中写死 Windows 路径、 盘符或 Windows 专用命令。
  • 路径处理统一使用 pathlib
  • ffmpeg 在 Linux 上可使用系统包,也允许通过 imageio-ffmpeg 使用内置 二进制,节点代码不能假设 ffmpeg 一定在 PATH。
  • 测试必须可以在 Windows 和 Linux 上运行;涉及平台分支的代码应同时覆盖 两种路径解析。

Windows / PowerShell 执行规则

  • 默认 shell 视为 Windows PowerShell 5.1;不要假设 Bash、zsh 或 PowerShell 7。 必要时先查 $PSVersionTable.PSVersion
  • 禁止把 Bash 语法交给 PowerShellpython - <<'PY'cat <<EOFexportsourcerm -rf、Bash 后台 & 等。
  • PowerShell 中 & 是调用运算符;URL 或参数含 & 时整体单引号引用。
  • 避免 PowerShell 5.1 下使用 Bash 风格 && / ||;顺序步骤用多行 PowerShell。
  • 参数含空格、括号、中文、&|;><$ 或引号时默认用单引号。
  • 外部程序路径可能有空格时,用 & 'C:\path with spaces\tool.exe' arg1
  • 文件操作优先 PowerShell 原生命令和 -LiteralPath
  • 复杂 Python 不用 python -c;涉及 SQL、JSON、中文、反斜杠路径、换行或 多层引号时,用仓库脚本或临时 .py 文件。
  • 禁止在 PowerShell 用 Bash here-doc。临时传 Python 源码只允许 PowerShell here-string,且尽量保持 ASCII。
  • Python 源码含中文常量时,不通过 PowerShell 管道传给 python -;用 UTF-8 脚本文件、仓库脚本或 \uXXXX
  • 搜索文本/文件优先 rg / rg --files
  • 数据库或生产内容写操作前先查询当前数据;写入必须有明确筛选条件,禁止 无条件 DELETE / UPDATE
  • 同一 PowerShell 命令连续失败两次后,停止微调长命令;改短命令、脚本文件、 数组 splatting 或分步验证。

关键设计约束(北极星不变式)

  • 节点之间不直接调用,只通过产物 URI 交换数据;中间产物落在共享存储 (data/storage),不放在节点模块内部。
  • 工作流必须是数据文件或数据库记录(workflow_versions 表存 DAG JSON), 不允许把步骤顺序写死在应用代码里。
  • 节点注册表是节点调用的唯一入口;API 与调度器不绕过 registry 直接执行 节点逻辑。
  • 协议数据模型(wov_sdk)要长期稳定,宁可先少做功能,也不轻易改协议。
  • 存储、队列、调度器都要通过抽象边界隔离,方便从单机实现替换为分布式实现。
  • 用户端永远只看到"输入 -> 进度 -> 结果",不暴露工作流细节。
  • 单体对分布式版的三处降级:无子进程隔离、无空闲 TTL 回收(模型常驻, 仅懒加载)、非 OCR 慢任务无法强制中断(由节点自身超时兜底;OCR 节点支持 暂停信号逐帧中断)。

单体化说明

  • 由原 7 个独立仓库合并:wov-api、wov-web、wov-sdk、wov-node-echo、 wov-node-ffmpeg、wov-node-whisper、wov-node-llm、wov-node-ass。
  • 删除内容:NodeManager(子进程生命周期)、节点 HTTP 服务端 (wov_sdk.server)、节点注册/实例管理 API 与页面、node_instances 表、 各节点的 __main__ 进程入口。
  • 保留内容:协议数据模型、工作流 DAG 数据化、调度拓扑执行、上传/进度/下载/ 重试 API、静态前端、SQLite Repository 层、本地优先模型加载。