feat: llm-filter 节点级断点存档
- 每条 LLM 判定成功立即追加 filter_partial.jsonl(多线程加锁串行化) - 失败/中断后重跑只重判未判定条目,已判定结果复用,与 OCR 存档同机制 - 附带上下文净化回归重跑脚本(run_011d01f19999)
This commit is contained in:
@@ -709,3 +709,111 @@ def test_real_run_rules_and_dialogue_regression(monkeypatch, tmp_path) -> None:
|
||||
})
|
||||
assert len(fake.bodies) == unique_llm
|
||||
assert len(fake.bodies) < len(entries) # 去重确实省调用。
|
||||
|
||||
|
||||
class _FailOnTarget429:
|
||||
"""模拟持续限流:目标字幕含指定词时恒抛 429,其余正常返回 dialogue。
|
||||
|
||||
用于构造"部分条目成功、个别条目持续 429"的断点重跑场景:第一次 invoke
|
||||
整体失败但成功条目已写存档;解除限流后第二次 invoke 只重判失败条目。
|
||||
"""
|
||||
|
||||
def __init__(self, marker: str) -> None:
|
||||
self.marker = marker
|
||||
self.enabled = True
|
||||
self.bodies: list[dict] = []
|
||||
|
||||
def __call__(self, request, timeout=None):
|
||||
body = json.loads(request.data.decode("utf-8"))
|
||||
self.bodies.append(body)
|
||||
target = next(
|
||||
line for line in body["messages"][1]["content"].splitlines()
|
||||
if line.startswith("【目标】")
|
||||
)
|
||||
if self.enabled and self.marker in target:
|
||||
raise urllib.error.HTTPError(request.full_url, 429, "rate limited", {}, None)
|
||||
payload = json.dumps({"choices": [{"message": {"content": "dialogue"}}]}).encode()
|
||||
return FakeResponse(payload)
|
||||
|
||||
|
||||
def _llm_invoke(srt_text: str, out: Path) -> tuple[object, Path]:
|
||||
"""用给定 SRT 文本构造并执行一次 llm-filter invoke,返回 (响应, 输入文件)。"""
|
||||
srt = out.parent / "in.srt"
|
||||
srt.write_text(srt_text, encoding="utf-8")
|
||||
return (
|
||||
invoke(
|
||||
InvokeRequest(
|
||||
run_id="r", node_instance_id="",
|
||||
inputs={"srt_uri": str(srt)},
|
||||
params={},
|
||||
output_dir=str(out),
|
||||
)
|
||||
),
|
||||
srt,
|
||||
)
|
||||
|
||||
|
||||
def test_load_and_append_partial(tmp_path) -> None:
|
||||
"""判定存档读写:无存档/损坏行跳过,追加后可读回。"""
|
||||
from nodes.llm_filter import _PARTIAL_NAME, _append_partial, _load_partial
|
||||
|
||||
out = tmp_path / "out"
|
||||
assert _load_partial(out) == {} # 目录不存在 → 空。
|
||||
out.mkdir()
|
||||
assert _load_partial(out) == {} # 无存档 → 空。
|
||||
# 损坏行(半行写入)跳过,正常行读回。
|
||||
(out / _PARTIAL_NAME).write_text(
|
||||
'{"index": 0, "category": "dialogue"}\n\n{broken\n{"index": 3, "category": "garbage"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert _load_partial(out) == {0: "dialogue", 3: "garbage"}
|
||||
# 追加一条后读回。
|
||||
_append_partial(out, 5, "overlay")
|
||||
assert _load_partial(out)[5] == "overlay"
|
||||
|
||||
|
||||
def test_invoke_resume_skips_archived_judgments(monkeypatch, tmp_path) -> None:
|
||||
"""断点存档:已判定条目重跑时不重复调用 LLM(去重后只补判未判定)。"""
|
||||
fake = _patch_llm(monkeypatch, contents=["dialogue"] * 4)
|
||||
out = tmp_path / "out"
|
||||
out.mkdir()
|
||||
# 预置存档:索引 0、1 已判定(模拟上次失败前已完成的部分)。
|
||||
from nodes.llm_filter import _append_partial
|
||||
|
||||
_append_partial(out, 0, "dialogue")
|
||||
_append_partial(out, 1, "dialogue")
|
||||
response, _srt = _llm_invoke(_SRT, out)
|
||||
assert response.status == "completed", response.error
|
||||
# 4 条唯一文本中 2 条已存档,只调用剩余 2 条。
|
||||
assert len(fake.bodies) == 2
|
||||
# 输出与全量判定一致:全部 dialogue → 4 条都保留。
|
||||
assert response.outputs["kept"] == 4
|
||||
assert Path(response.outputs["srt_uri"]).read_text(encoding="utf-8").count("-->") == 4
|
||||
|
||||
|
||||
def test_invoke_partial_failure_then_resume(monkeypatch, tmp_path) -> None:
|
||||
"""真实断点重跑:个别条目持续 429 → 整体失败但成功判定已写存档,
|
||||
解除限流后重跑只补判失败条目,最终产物与一次跑完一致。"""
|
||||
fake = _FailOnTarget429(marker="无意义")
|
||||
monkeypatch.setattr("nodes.llm_filter.urllib.request.urlopen", fake)
|
||||
monkeypatch.setattr("nodes.llm_filter.time.sleep", lambda s: None)
|
||||
out = tmp_path / "out"
|
||||
first, _srt = _llm_invoke(_SRT, out)
|
||||
assert first.status == "failed"
|
||||
assert "429" in (first.error or "")
|
||||
calls_first = len(fake.bodies)
|
||||
# 成功判定的 3 条已写存档;持续 429 的"答:无意义杂项"(索引 2)不在存档。
|
||||
from nodes.llm_filter import _load_partial
|
||||
|
||||
partial = _load_partial(out)
|
||||
assert 2 not in partial
|
||||
assert len(partial) == 3
|
||||
# 解除限流后重跑:只补判失败条目(1 次调用),其余复用存档。
|
||||
fake.enabled = False
|
||||
second, _srt = _llm_invoke(_SRT, out)
|
||||
assert second.status == "completed", second.error
|
||||
assert len(fake.bodies) == calls_first + 1
|
||||
# 输出:解除限流后全部判定为 dialogue → 4 条都保留(与"一次跑完"一致)。
|
||||
out_text = Path(second.outputs["srt_uri"]).read_text(encoding="utf-8")
|
||||
assert out_text.count("-->") == 4
|
||||
assert "00:00:09,000" in out_text
|
||||
|
||||
Reference in New Issue
Block a user