fix: SRT 按 cue 解析并按 ID 回填译文,OCR 空帧分段与失败重试
This commit is contained in:
+50
-31
@@ -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 = {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user