fix: SRT 按 cue 解析并按 ID 回填译文,OCR 空帧分段与失败重试
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
目标:验证 subtitle-ocr 在**多线程**执行时能否正确处理字幕顺序。
|
||||
|
||||
R06 更新:下述 1666 条文件保留作历史记录,含跨空白帧合并缺陷,已不作为
|
||||
逐字节正确性标准。基线由真实单线程/完整成功存档组装产生(1942 条),
|
||||
同时逐采样点检查正文和空白,要求多线程及断点结果与新基线一致。
|
||||
|
||||
- 不走真实 vlm-ocr(Ollama)网络调用: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 必须跳过,不视为一条记录。
|
||||
|
||||
Reference in New Issue
Block a user