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
+50 -31
View File
@@ -56,7 +56,11 @@ def _load_partial(output_dir: Path) -> dict[int, str]:
except json.JSONDecodeError:
# 进程被杀时可能残留半行写入:跳过该行,对应帧视为未处理。
continue
result[int(item["frame"])] = str(item["text"])
# 新存档区分成功空帧与确定跳过(超长输出);失败不写成功存档。
# 旧版空串可能来自网络故障,恢复时重新识别;旧版非空结果仍可复用。
text = str(item["text"])
if item.get("status") in ("completed", "skipped") or ("status" not in item and text):
result[int(item["frame"])] = text
return result
@@ -139,7 +143,7 @@ def _merge_kept(
if not text:
continue
time = float(manifest[index]["time"])
if kept and kept[-1][2] == text:
if kept and index > 0 and texts[index - 1] == text:
kept[-1] = (kept[-1][0], time, text)
continue
kept.append((time, time, text))
@@ -185,8 +189,8 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
len(partial_texts), len(manifest), len(pending),
)
# 单帧 OCR并行池的工作函数,返回该帧识别文本(失败/空/超长均返回空串)
# 每帧无论结果如何都把 {frame, text} 追加到断点存档,重启后不再重跑该帧
# 单帧 OCR成功返回文字或空串;临时失败抛异常,不写成功存档
# 超长输出按既有规则确定跳过,以 skipped 存档区别于成功无文字
def ocr_frame(payload) -> str:
index, item = payload
# 暂停检查:调度器置 PAUSED 并向 run 根写入 paused.flag 后,工作线程
@@ -197,33 +201,39 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
pool.cancel()
raise PauseRequested(f"OCR 被暂停(run {request.run_id}")
text = ""
response = registry.invoke(
"vlm-ocr",
InvokeRequest(
run_id=request.run_id,
node_instance_id="",
inputs={"image_uri": str(item["image_uri"])},
params=vlm_params,
output_dir=str(Path(request.output_dir) / "ocr_frames" / f"{index:04d}"),
),
)
if response.status != "completed":
# 单帧失败不中断整体,跳过该帧继续汇总。
logger.warning("%d OCR 失败,跳过: %s", index, response.error)
else:
logger.info("%d/%d OCR 完成: %r", index + 1, len(manifest), response.outputs.get("text"))
text = str(response.outputs.get("text", "")).strip()
if len(text) > max_result_chars:
# 超长输出视为模型异常(重复循环等),直接报错并跳过该帧。
logger.warning(
"%d OCR 输出超长(%d > %d),跳过: %r",
index, len(text), max_result_chars, text[:60],
)
text = ""
# 断点存档:成功/失败/空串都记录"已处理",恢复时保持一致行为。
try:
response = registry.invoke(
"vlm-ocr",
InvokeRequest(
run_id=request.run_id,
node_instance_id="",
inputs={"image_uri": str(item["image_uri"])},
params=vlm_params,
output_dir=str(Path(request.output_dir) / "ocr_frames" / f"{index:04d}"),
),
)
if response.status != "completed":
raise RuntimeError(f"{index} OCR 失败: {response.error}")
if not isinstance(response.outputs.get("text"), str):
raise ValueError(f"{index} OCR 缺少有效 text")
except Exception:
pool.report_failure()
raise
status = "completed"
logger.info("%d/%d OCR 完成: %r", index + 1, len(manifest), response.outputs.get("text"))
text = response.outputs["text"].strip()
if len(text) > max_result_chars:
# 超长输出视为模型异常(重复循环等),直接报错并跳过该帧。
logger.warning(
"%d OCR 输出超长(%d > %d),跳过: %r",
index, len(text), max_result_chars, text[:60],
)
text = ""
status = "skipped"
# 只存成功或确定跳过的结果,网络故障保持未处理,恢复时重新识别。
with _partial_lock:
with partial_path.open("a", encoding="utf-8") as fh:
fh.write(json.dumps({"frame": index, "text": text}, ensure_ascii=False) + "\n")
fh.write(json.dumps({"frame": index, "text": text, "status": status}, ensure_ascii=False) + "\n")
return text
if pending:
@@ -255,8 +265,17 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
# resume 后从剩余帧续跑;以 failed 返回让调度器保持 PAUSED(不误报失败)。
if any(isinstance(text, PauseRequested) for text in pending_texts):
return InvokeResponse(status="failed", error=f"OCR 被暂停(run {request.run_id}")
# 新增结果按帧号归位,与断点存档合并成完整帧序文本列表
new_by_index = {i: t for (i, _item), t in zip(pending, pending_texts)}
# 失败帧在收紧后的并发额度下重试一轮,成功帧(含空帧)不重复调用
failed = [payload for payload, text in zip(pending, pending_texts) if isinstance(text, Exception)]
new_by_index = {i: t for (i, _item), t in zip(pending, pending_texts) if not isinstance(t, Exception)}
if failed:
logger.warning("OCR %d 帧失败,重试一次", len(failed))
retried = pool.map(failed)
for (index, _item), text in zip(failed, retried):
if isinstance(text, Exception):
# 不发布残缺字幕;已成功写盘的其他帧下次直接复用。
return InvokeResponse(status="failed", error=f"OCR 帧 {index} 重试失败: {text}")
new_by_index[index] = text
else:
new_by_index = {}