fix: SRT 按 cue 解析并按 ID 回填译文,OCR 空帧分段与失败重试

This commit is contained in:
2026-09-11 17:19:25 +08:00
parent 3a612919f7
commit 6eb65e4356
9 changed files with 391 additions and 326 deletions
+24 -5
View File
@@ -2,6 +2,10 @@
目标:验证 subtitle-ocr 在**多线程**执行时能否正确处理字幕顺序。
R06 更新:下述 1666 条文件保留作历史记录,含跨空白帧合并缺陷,已不作为
逐字节正确性标准。基线由真实单线程/完整成功存档组装产生(1942 条),
同时逐采样点检查正文和空白,要求多线程及断点结果与新基线一致。
- 不走真实 vlm-ocrOllama)网络调用:registry.invoke 被替换为
FakeVlmOcrApi,按 image_uri 文件名中的帧号,直接从测试数据(真实任务
run_ac7f480a3ccb 的**全量**逐帧 OCR 结果)取该帧文本返回,模拟真实
@@ -111,7 +115,9 @@ def _run_ocr(monkeypatch, manifest_path: Path, texts_by_frame: dict[int, str],
"""用给定线程配置运行 subtitle-ocr,返回 (产物路径, 假 API 实例)。"""
from nodes.subtitle_ocr import invoke as ocr_invoke
fake = FakeVlmOcrApi(texts_by_frame, seed=seed)
# 单线程基线无需模拟网络等待,多线程仍保留真实延迟分布验证乱序完成。
fake = (FakeVlmOcrApi(texts_by_frame, seed=seed, fast_ms=0, slow_ms=0)
if pool_max == 1 else FakeVlmOcrApi(texts_by_frame, seed=seed))
monkeypatch.setattr("wov_app.registry.invoke", fake)
response = ocr_invoke(
InvokeRequest(
@@ -157,6 +163,11 @@ def _assert_alignment(srt_text: str, manifest: list[dict], texts_by_frame: dict[
best = min(time_text, key=lambda t: abs(t - start_s))
assert abs(best - start_s) <= 0.002, f"字幕起始时刻 {start_s}s 无对应帧"
assert time_text[best] == text.strip(), f"时刻 {start_s}s 的文本与帧不一致"
# 每个采样点都须匹配正文,不能让同一句字幕跨过无文字帧。
end_s = _ts_to_seconds(_end)
covered = [value for timestamp, value in time_text.items()
if best <= timestamp < end_s - 0.002]
assert covered and all(value == text.strip() for value in covered)
times.append(start_s)
assert all(a < b for a, b in zip(times, times[1:])), "时间轴必须严格递增"
@@ -174,7 +185,11 @@ class TestSubtitleOcrOrderUnderThreading:
def test_full_real_data_variable_latency_keeps_order(self, monkeypatch, tmp_path) -> None:
"""全量真实数据 + 真实可变延迟:多线程产物与单线程确认结果逐字节一致。"""
manifest, texts_by_frame = _load_full_data()
confirmed = CONFIRMED_SRT.read_text(encoding="utf-8")
# R06:旧 1666 条结果跨空白合并,改用当前真实单线程路径构建基线。
confirmed_path, _ = _run_ocr(monkeypatch, FULL_MANIFEST, texts_by_frame,
1, 1, tmp_path / "single", 20260817)
confirmed = confirmed_path.read_text(encoding="utf-8")
assert confirmed.count("-->") == 1942
outputs: dict[tuple, str] = {}
fakes: dict[tuple, FakeVlmOcrApi] = {}
@@ -188,7 +203,7 @@ class TestSubtitleOcrOrderUnderThreading:
outputs[(pool_min, pool_max)] = srt_path.read_text(encoding="utf-8")
fakes[(pool_min, pool_max)] = fake
# ① 多线程产物与用户确认过的精确结果(真实单线程运行逐字节一致。
# ① 多线程产物与本次真实单线程运行基线逐字节一致R06 修正跨空白)
assert outputs[(4, 4)] == confirmed, "4 线程产物与确认结果不一致"
assert outputs[(16, 16)] == confirmed, "16 线程产物与确认结果不一致"
assert outputs[(4, 4)] == outputs[(16, 16)]
@@ -223,7 +238,11 @@ def test_ocr_resumes_from_partial_checkpoint(monkeypatch, tmp_path) -> None:
跑完逐字节一致——重启不浪费已处理的帧。
"""
manifest, texts_by_frame = _load_full_data()
confirmed = CONFIRMED_SRT.read_text(encoding="utf-8")
# 基线走完整 OCR 路径,包含超长输出跳过规则,不手写业务处理后的存档。
baseline_path, _ = _run_ocr(monkeypatch, FULL_MANIFEST, texts_by_frame,
1, 1, tmp_path / "baseline", 99)
confirmed = baseline_path.read_text(encoding="utf-8")
assert confirmed.count("-->") == 1942
out_dir = tmp_path / "resume"
partial_path = out_dir / "ocr_partial.jsonl"
partial_path.parent.mkdir(parents=True)
@@ -231,7 +250,7 @@ def test_ocr_resumes_from_partial_checkpoint(monkeypatch, tmp_path) -> None:
for i in range(100):
# 存档按 0-based 帧序号记录;manifest[i] 的帧号 = i+1。
lines.append(
json.dumps({"frame": i, "text": texts_by_frame[i + 1]}, ensure_ascii=False)
json.dumps({"frame": i, "text": texts_by_frame[i + 1], "status": "completed"}, ensure_ascii=False)
)
if i == 50:
lines.append("") # 空行:_load_partial 必须跳过,不视为一条记录。