确认所有运行时引用均使用 V2(V3 已停用),并修复一处真实不一致: - 工作流数据文件:learn-translate 用 faster-whisper-large-v2、 zh-direct 用 whisper-large-v2-translate-zh-v0.2-st-ct2(原本即 V2); - 本地库的 demo 最新版本仍指向 large-v3:workflows/demo.json 早已改为 V2, 但 seed 对已存在工作流刻意跳过,导致旧库停留在历史上用 V3 保存的定义, 即本机跑 demo 实际加载 V3 权重。按用户决定移除 demo 工作流及其关联的 6 个 run、1 个批量任务与 431 条明细(媒体库中已放置的 6 个字幕成品保留); - nodes/whisper.py 候选与远端兜底本就是 large-v2; - V3 权重目录保留在盘上仅作对照实验,文档标注为废弃; scripts/compare_whisper_v2_vs_v3.py 保留用于对照。 顺带修复与清理: - src/wov_app/scheduler.py:_file_size 补捕 ValueError(见上一条提交说明 的真实缺陷,此处为同一批改动); - .gitignore:data/ 改为 /data/,避免连带忽略 tests/**/data/; - scripts/*:评测集路径改到 scripts/data/translate_eval/; - 代码注释与文档同步移除 demo 引用(历史调研文档保留说明性引用)。 验证:全量 477 passed;新库 seed 只创建 3 个 V2 工作流。
106 lines
4.4 KiB
Python
106 lines
4.4 KiB
Python
"""从最终中文字幕反查各类"片段窗口",供人工挑选评测用例。
|
||
|
||
背景与目的
|
||
----------
|
||
评测翻译模型需要"原文 + 该原文对应的真实含义"的成对基准。本仓库同一部视频
|
||
(CJOD-255,2 小时)有两条真实产物:
|
||
|
||
- 日语原文:run_d386ccf124f7(learn-translate)的 whisper ASR,
|
||
1161 条 cue(约每 1-4 句一条);
|
||
- 中文基准:run_479b411f299d(ocr-subtitle)过滤后的烧录字幕 1144 条
|
||
——烧录字幕是画面的一部分,也可视为官方/人工字幕,但它**是一条条按屏幕
|
||
显示合并的整行**(约 2-8 秒一条),与 whisper 的 cue 切分不同。
|
||
|
||
因此不能逐条对照。正确口径是**窗口级**:取中文基准一条字幕的时间窗,收集该窗
|
||
内覆盖的日语 cue(1-3 条),窗口即为一个评测用例——比较"模型对这几条 cue 的
|
||
译文合起来"是否表达了该中文行。本脚本把这类窗口导出,供人工确认与挑选。
|
||
|
||
用法:
|
||
uv run python scripts/build_segment_eval.py --dump-window 1150 # 看某处的窗口细节
|
||
uv run python scripts/build_segment_eval.py --export scripts/data/translate_eval/windows.jsonl
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
from pathlib import Path
|
||
|
||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||
|
||
# 两条真实产物(相对仓库根)。
|
||
DEFAULT_JA = PROJECT_ROOT / "data/storage/runs/run_d386ccf124f7/steps/asr/transcript.srt"
|
||
DEFAULT_ZH = PROJECT_ROOT / "data/storage/runs/run_479b411f299d/steps/filter/filtered.srt"
|
||
|
||
|
||
def seconds(ts: str) -> float:
|
||
"""SRT 时间戳 -> 秒。"""
|
||
hours, minutes, rest = ts.split(":")
|
||
secs, millis = rest.split(",")
|
||
return int(hours) * 3600 + int(minutes) * 60 + int(secs) + int(millis) / 1000
|
||
|
||
|
||
def build_windows(ja_path: Path, zh_path: Path, max_ja: int = 3, max_duration: float = 15.0) -> list[dict]:
|
||
"""返回可用评测窗口列表。
|
||
|
||
窗口 = 一条中文基准字幕 + 其时间窗内的日语 cue 组(1..max_ja 条)。
|
||
过滤条件:窗口时长 <= max_duration(避免整段合并的长窗口无法逐句核对)、
|
||
cue 数在 1..max_ja 之间(0 条说明 OCR 行无对应日语,>max_ja 说明配对太碎)。
|
||
"""
|
||
from nodes.srt import parse_srt
|
||
|
||
ja_cues = parse_srt(ja_path.read_text(encoding="utf-8"))
|
||
zh_cues = parse_srt(zh_path.read_text(encoding="utf-8"))
|
||
|
||
windows: list[dict] = []
|
||
for zh in zh_cues:
|
||
start, end = seconds(zh.start), seconds(zh.end)
|
||
group = [c for c in ja_cues if seconds(c.start) < end and start < seconds(c.end)]
|
||
if not 1 <= len(group) <= max_ja or (end - start) > max_duration:
|
||
continue
|
||
windows.append(
|
||
{
|
||
"zh_start": zh.start,
|
||
"zh_end": zh.end,
|
||
"zh_ref": zh.text,
|
||
"ja_start": group[0].start,
|
||
"ja_end": group[-1].end,
|
||
"ja_lines": [c.text for c in group],
|
||
"ja_ids": [ja_cues.index(c) + 1 for c in group],
|
||
}
|
||
)
|
||
return windows
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser(description="导出窗口级翻译评测候选")
|
||
parser.add_argument("--ja", type=Path, default=DEFAULT_JA)
|
||
parser.add_argument("--zh", type=Path, default=DEFAULT_ZH)
|
||
parser.add_argument("--export", type=Path, default=None, help="写出 windows.jsonl")
|
||
parser.add_argument("--dump-window", type=float, default=None, help="打印指定秒附近的窗口")
|
||
parser.add_argument("--limit", type=int, default=0)
|
||
args = parser.parse_args()
|
||
|
||
windows = build_windows(args.ja, args.zh)
|
||
print(f"可用窗口 {len(windows)} 个(1-3 条日语 cue 且窗口 <=15s)")
|
||
|
||
if args.export:
|
||
args.export.parent.mkdir(parents=True, exist_ok=True)
|
||
with args.export.open("w", encoding="utf-8") as fh:
|
||
for index, item in enumerate(windows, 1):
|
||
fh.write(json.dumps({"id": index, **item}, ensure_ascii=False) + "\n")
|
||
print(f"已写入 {args.export}")
|
||
|
||
if args.dump_window is not None:
|
||
for item in windows:
|
||
if abs(seconds(item["zh_start"]) - args.dump_window) <= 12:
|
||
print(json.dumps(item, ensure_ascii=False, indent=1))
|
||
|
||
for item in (windows if args.limit <= 0 else windows[: args.limit]):
|
||
joined = " | ".join(item["ja_lines"])
|
||
print(f"{item['zh_start']} JA[{len(item['ja_lines'])}]: {joined!r} -> {item['zh_ref']!r}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|