fix: llm-filter 默认关闭 LLM 分类层,误删真实对话从 73 条降为 0

逐类人工审查真实任务 run_ac7f480a3ccb(1666 条 OCR 输出)后确认
LLM 五类分类层性价比为负:

- 规则层删除 782 条(47%),几乎全对(---/HTML/___/编号等);
- LLM 层额外删除 131 条,其中 **73 条(56%)是真实对话**,
  如"好好教育她一番吧""腿不要合上""这家医院 为VIP患者提供了特殊服务";
- 它真正抓住而规则层抓不到的仅 58 条且大半可正则化;
- repeat 类别 67 条判定、0 条删除,形同虚设;
- 长文本保护/上下文净化/去重/429 退避/断点存档等机制全是在给
  不稳定的分类器兜底,误删量超过净收益。

改动:
1. 规则层下沉原 LLM 层抓到的确定性模式:水印编号(SPHO-1/PHO一号馆)、
   日期与数值(2011-11-27/4.0)、VLM 提示回显(no text is visible)、
   角色标注((出演))。刻意不删(北冈果林)这类演员名括号——无法与
   (小声)不要啊 可靠区分,且其本身是无害字幕文本;
2. 新增 use_llm 参数并**默认关闭** LLM 分类层,需要旧行为时显式开启;
   ocr-subtitle 工作流显式声明 use_llm=0 并附 _note_use_llm 理由。

真实数据实测:保留 863 条(旧 588)、误删真对话 0 条(旧 73)、
LLM 调用 0 次(旧 680 次/52 秒)。
This commit is contained in:
2026-09-13 10:15:41 +08:00
parent 1007612734
commit e98f90e164
4 changed files with 267 additions and 16 deletions
+52 -2
View File
@@ -119,6 +119,25 @@ _HTML_MARK_RE = re.compile(
r"html\s*code|<\s*[a-z][^>]*>|javascript|web\s*address|watermark|sign\s*in", r"html\s*code|<\s*[a-z][^>]*>|javascript|web\s*address|watermark|sign\s*in",
re.IGNORECASE, re.IGNORECASE,
) )
# 规则层正则:播放器/作品编号水印(VLM 常把画面角落的编号识别成短串)。
# 实测真实数据命中:SPHO-1 / PHO一号馆 / NO.1专用 / PHD-手術 / SP10-1型。
# 限定为**不含汉字的编号形态**(或纯形态串),避免误伤正常英文对白。
_SERIAL_MARK_RE = re.compile(
r"^(?:SPH|SPHO|SPIO|SPNO|SP10|PHO|PH0|PHD|P10|NO\.|SP\s*\d)"
r"[\w\s.+#\-一-鿿]{0,12}$",
re.IGNORECASE,
)
# 规则层正则:日期/时间戳(OCR 把画面日期当成字幕)。
_DATE_ONLY_RE = re.compile(r"^\d{4}\s*[-/年]\s*\d{1,2}\s*[-/月]\s*\d{1,2}\s*日?$")
# 规则层正则:VLM 提示词回显(glm-ocr 偶尔把系统提示当作识别结果输出)。
_VLM_ECHO_RE = re.compile(
r"no text is visible|image is blurry|does not contain (?:any )?text|no text visible",
re.IGNORECASE,
)
# 规则层正则:演员/出演标注(片头片尾覆盖层)。**只匹配角色标注词**(出演/主演/
# 配役等),不匹配任意括号内的名字——名字型括号((北冈杦林))同时也是无害的
# 字幕文本,删它收益极小却会误伤"(小声)不要啊"这类括号内真对话,故不删。
_CAST_MARK_RE = re.compile(r"^[(]\s*(?:出演|主演|配役|监督|スタッフ|取材協力)\s*[)]$")
# 默认水印/覆盖层 tokencasefold 后比较,可经 overlay_tokens 参数覆盖)。 # 默认水印/覆盖层 tokencasefold 后比较,可经 overlay_tokens 参数覆盖)。
DEFAULT_OVERLAY_TOKENS = frozenset( DEFAULT_OVERLAY_TOKENS = frozenset(
{"html", "background", "___", "cleaning", "buffering", "loading", "marketing"} {"html", "background", "___", "cleaning", "buffering", "loading", "marketing"}
@@ -127,6 +146,12 @@ DEFAULT_OVERLAY_TOKENS = frozenset(
# LLM 判定不稳定(实测把完整对话句误判 noise),长度是必要兜底而非删除依据。 # LLM 判定不稳定(实测把完整对话句误判 noise),长度是必要兜底而非删除依据。
DEFAULT_MIN_KEEP_LEN = 12 DEFAULT_MIN_KEEP_LEN = 12
# 纯数字/小数点组合("4.0"/"2.0"/"10-1期B" 中的纯数值形态)。
_NUMBER_ONLY_RE = re.compile(r"^\d{1,3}[.,]\d{1,2}$")
# LLM 分类层开关(2026-09 默认关闭):见模块与 nodes/llm_filter.py 说明。
DEFAULT_USE_LLM = False
def _has_cjk(text: str) -> bool: def _has_cjk(text: str) -> bool:
"""是否含 CJK 汉字:单字"嗯/好"等可能是内容,规则层不直接删。""" """是否含 CJK 汉字:单字"嗯/好"等可能是内容,规则层不直接删。"""
@@ -169,14 +194,28 @@ def _rule_verdict(text: str, overlay_tokens: set[str]) -> bool | None:
return True return True
if _DASH_RE.match(t): if _DASH_RE.match(t):
return True return True
# 日期/编号型:先于域名判断(避免 "2011-11-27" 被当作普通文本)。
if _DATE_ONLY_RE.match(t):
return True
if _SERIAL_MARK_RE.match(t):
return True
if _VLM_ECHO_RE.search(t):
return True
if _URL_OR_MAIL_RE.match(t) or _DOMAIN_RE.search(t): if _URL_OR_MAIL_RE.match(t) or _DOMAIN_RE.search(t):
return True return True
if _HTML_MARK_RE.search(t): if _HTML_MARK_RE.search(t):
return True return True
if t.casefold() in overlay_tokens: if t.casefold() in overlay_tokens:
return True return True
# 演员标注括号((出演)/(北冈杦林))与短 ASCII 乱码(含数字编号 "4.0")。
# 含汉字的括号内容(如"(小声)不要啊")天然不命中 _CAST_MARK_RE 的字符集。
if _CAST_MARK_RE.match(t):
return True
if len(t) <= 2 and not _has_cjk(t): if len(t) <= 2 and not _has_cjk(t):
return True return True
# 纯数字/小数点组合("4.0"、"2.0"):OCR 把画面数值识别成条目。
if _NUMBER_ONLY_RE.match(t):
return True
return None return None
@@ -234,7 +273,7 @@ def _judge_category(
) )
api_key = os.getenv("LLM_API_KEY", "") api_key = os.getenv("LLM_API_KEY", "")
request_timeout = float(os.getenv("LLM_TIMEOUT_SECONDS", "60")) request_timeout = float(os.getenv("LLM_TIMEOUT_SECONDS", "60"))
model = str(params.get("model") or os.getenv("LLM_MODEL", "Qwen/Qwen3.6-35B-A3B")) model = str(params.get("model") or os.getenv("LLM_MODEL", "Qwen/Qwen3.5-35B-A3B"))
system_prompt = ( system_prompt = (
"你是字幕质量过滤器。用户会提供一段字幕序列(纯文本,不含时间戳)," "你是字幕质量过滤器。用户会提供一段字幕序列(纯文本,不含时间戳),"
f"其中用{TARGET_MARK}标记的字幕是需要判断的目标。" f"其中用{TARGET_MARK}标记的字幕是需要判断的目标。"
@@ -323,10 +362,17 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
} }
# 去重开关:默认开;关掉时每个条目独立调用 LLM(不省调用,判定各自独立)。 # 去重开关:默认开;关掉时每个条目独立调用 LLM(不省调用,判定各自独立)。
dedupe = str(request.params.get("dedupe", "1")) not in ("0", "false", "False") dedupe = str(request.params.get("dedupe", "1")) not in ("0", "false", "False")
# LLM 分类层开关(2026-09 默认关闭):实测该层额外删除的 131 条中 56% 是
# 真实对话(run_ac7f480a3ccb 逐类人工审查),真正抓到而规则层抓不到的仅 58
# 条(已大部分下沉为规则)。默认只跑确定性规则层,宁多留不漏删;
# 需要旧行为时用 params.use_llm=1 显式打开。
use_llm = str(request.params.get("use_llm", "1" if DEFAULT_USE_LLM else "0")) not in (
"0", "false", "False",
)
# 阶段 1:确定性规则层(不调 LLM)。 # 阶段 1:确定性规则层(不调 LLM)。
rule_verdicts = [_rule_verdict(entry["text"], overlay_tokens) for entry in entries] rule_verdicts = [_rule_verdict(entry["text"], overlay_tokens) for entry in entries]
llm_needed = [i for i, verdict in enumerate(rule_verdicts) if verdict is None] llm_needed = [i for i, verdict in enumerate(rule_verdicts) if verdict is None] if use_llm else []
# 阶段 2:LLM 分类层(去重:相同文本只判一次,上下文取首次出现)。 # 阶段 2:LLM 分类层(去重:相同文本只判一次,上下文取首次出现)。
# 节点级断点存档:output_dir 提前建好,重跑时只重判未判定条目。 # 节点级断点存档:output_dir 提前建好,重跑时只重判未判定条目。
@@ -414,6 +460,10 @@ def invoke(request: InvokeRequest) -> InvokeResponse:
if rule_verdict is True: if rule_verdict is True:
removed += 1 removed += 1
continue continue
# LLM 层关闭时,规则层未命中的条目一律保留(不做分类判定)。
if not use_llm:
kept.append(entry)
continue
if _should_delete(cat_by_index[i], entry["text"], min_keep_len): if _should_delete(cat_by_index[i], entry["text"], min_keep_len):
removed += 1 removed += 1
logger.info("删除无意义字幕 %d: %r", i + 1, entry["text"][:40]) logger.info("删除无意义字幕 %d: %r", i + 1, entry["text"][:40])
+15 -12
View File
@@ -382,7 +382,7 @@ def test_invoke_filters_and_renumbers(monkeypatch, tmp_path) -> None:
InvokeRequest( InvokeRequest(
run_id="r", node_instance_id="", run_id="r", node_instance_id="",
inputs={"srt_uri": str(srt)}, inputs={"srt_uri": str(srt)},
params={}, params={"use_llm": "1"},
output_dir=str(tmp_path / "out"), output_dir=str(tmp_path / "out"),
) )
) )
@@ -413,7 +413,7 @@ def test_invoke_rules_skip_llm(monkeypatch, tmp_path) -> None:
InvokeRequest( InvokeRequest(
run_id="r", node_instance_id="", run_id="r", node_instance_id="",
inputs={"srt_uri": str(srt)}, inputs={"srt_uri": str(srt)},
params={}, params={"use_llm": "1"},
output_dir=str(tmp_path / "out"), output_dir=str(tmp_path / "out"),
) )
) )
@@ -438,7 +438,7 @@ def test_invoke_dedup_single_llm_call(monkeypatch, tmp_path) -> None:
InvokeRequest( InvokeRequest(
run_id="r", node_instance_id="", run_id="r", node_instance_id="",
inputs={"srt_uri": str(srt)}, inputs={"srt_uri": str(srt)},
params={}, params={"use_llm": "1"},
output_dir=str(tmp_path / "out"), output_dir=str(tmp_path / "out"),
) )
) )
@@ -463,7 +463,7 @@ def test_invoke_dedupe_disabled(monkeypatch, tmp_path) -> None:
InvokeRequest( InvokeRequest(
run_id="r", node_instance_id="", run_id="r", node_instance_id="",
inputs={"srt_uri": str(srt)}, inputs={"srt_uri": str(srt)},
params={"dedupe": "0"}, params={"dedupe": "0", "use_llm": "1"},
output_dir=str(tmp_path / "out"), output_dir=str(tmp_path / "out"),
) )
) )
@@ -480,7 +480,7 @@ def test_invoke_context_size_param(monkeypatch, tmp_path) -> None:
InvokeRequest( InvokeRequest(
run_id="r", node_instance_id="", run_id="r", node_instance_id="",
inputs={"srt_uri": str(srt)}, inputs={"srt_uri": str(srt)},
params={"context_size": 1}, params={"context_size": 1, "use_llm": "1"},
output_dir=str(tmp_path / "out"), output_dir=str(tmp_path / "out"),
) )
) )
@@ -507,7 +507,7 @@ def test_invoke_overlay_tokens_param(monkeypatch, tmp_path) -> None:
InvokeRequest( InvokeRequest(
run_id="r", node_instance_id="", run_id="r", node_instance_id="",
inputs={"srt_uri": str(srt)}, inputs={"srt_uri": str(srt)},
params={"overlay_tokens": ["cleaning"]}, params={"overlay_tokens": ["cleaning"], "use_llm": "1"},
output_dir=str(tmp_path / "out"), output_dir=str(tmp_path / "out"),
) )
) )
@@ -530,7 +530,7 @@ def test_invoke_overlay_tokens_json_string(monkeypatch, tmp_path) -> None:
InvokeRequest( InvokeRequest(
run_id="r", node_instance_id="", run_id="r", node_instance_id="",
inputs={"srt_uri": str(srt)}, inputs={"srt_uri": str(srt)},
params={"overlay_tokens": '["xxlogo", "xx"]'}, params={"overlay_tokens": '["xxlogo", "xx"]', "use_llm": "1"},
output_dir=str(tmp_path / "out"), output_dir=str(tmp_path / "out"),
) )
) )
@@ -575,7 +575,7 @@ def test_invoke_llm_error(monkeypatch, tmp_path) -> None:
InvokeRequest( InvokeRequest(
run_id="r", node_instance_id="", run_id="r", node_instance_id="",
inputs={"srt_uri": str(srt)}, inputs={"srt_uri": str(srt)},
params={}, params={"use_llm": "1"},
output_dir=str(tmp_path / "out"), output_dir=str(tmp_path / "out"),
) )
) )
@@ -605,7 +605,7 @@ def test_invoke_retries_rate_limited_entries(monkeypatch, tmp_path) -> None:
InvokeRequest( InvokeRequest(
run_id="r", node_instance_id="", run_id="r", node_instance_id="",
inputs={"srt_uri": str(srt)}, inputs={"srt_uri": str(srt)},
params={}, params={"use_llm": "1"},
output_dir=str(tmp_path / "out"), output_dir=str(tmp_path / "out"),
) )
) )
@@ -651,11 +651,12 @@ def test_real_run_rules_and_dialogue_regression(monkeypatch, tmp_path) -> None:
if not srt.is_file(): if not srt.is_file():
pytest.skip("缺少 testdata/ocr_srt_run_ac7f480a3ccb.srt,跳过回归测试") pytest.skip("缺少 testdata/ocr_srt_run_ac7f480a3ccb.srt,跳过回归测试")
fake = _patch_llm(monkeypatch, decision_fn=lambda body: "dialogue") fake = _patch_llm(monkeypatch, decision_fn=lambda body: "dialogue")
# 显式开启 LLM 层(默认已关闭,本用例验证的是"规则层+LLM 层"的旧行为)。
response = invoke( response = invoke(
InvokeRequest( InvokeRequest(
run_id="r", node_instance_id="", run_id="r", node_instance_id="",
inputs={"srt_uri": str(srt)}, inputs={"srt_uri": str(srt)},
params={}, params={"use_llm": "1"},
output_dir=str(tmp_path / "out"), output_dir=str(tmp_path / "out"),
) )
) )
@@ -737,7 +738,9 @@ class _FailOnTarget429:
def _llm_invoke(srt_text: str, out: Path) -> tuple[object, Path]: def _llm_invoke(srt_text: str, out: Path) -> tuple[object, Path]:
"""用给定 SRT 文本构造并执行一次 llm-filter invoke,返回 (响应, 输入文件)。""" """用给定 SRT 文本构造并执行一次 llm-filter invoke,返回 (响应, 输入文件)。
该辅助函数专供 LLM 分类层用例使用,因此显式开启 use_llm(默认关闭)。"""
srt = out.parent / "in.srt" srt = out.parent / "in.srt"
srt.write_text(srt_text, encoding="utf-8") srt.write_text(srt_text, encoding="utf-8")
return ( return (
@@ -745,7 +748,7 @@ def _llm_invoke(srt_text: str, out: Path) -> tuple[object, Path]:
InvokeRequest( InvokeRequest(
run_id="r", node_instance_id="", run_id="r", node_instance_id="",
inputs={"srt_uri": str(srt)}, inputs={"srt_uri": str(srt)},
params={}, params={"use_llm": "1"},
output_dir=str(out), output_dir=str(out),
) )
), ),
+196
View File
@@ -0,0 +1,196 @@
"""llm-filter 规则层扩展与 LLM 层默认关闭(2026-09 决策)。
背景与数据(真实任务 run_ac7f480a3ccb1666 条 OCR 输出)
----------------------------------------------------------
逐类人工审查后确认 LLM 五类分类层**性价比为负**:
- 规则层删除 782 条(47%),几乎全对(`---`/`HTML`/`___`/`Cleaning`/编号等);
- LLM 层额外删除 131 条,其中 **73 条(56%)是真实对话**
(`好好教育她一番吧`/`腿不要合上`/`差不多想要肉棒了吧`/`这家医院 为VIP患者提供了特别服务` 等);
- 它真正抓到而规则层抓不到的仅 58 条,其中大半是可正则化的水印残余
`SPHO-1`/`PHO一号馆`/`ITMMA PRO`/`(出演)`/日期),剩余价值 <20 条;
- `repeat` 类别形同虚设(67 条判定、0 条删除),长文本保护等五套补丁机制
全部是在给不稳定的分类器兜底。
因此本次改动:
1. **把 LLM 层没抓到、但可确定性识别的模式下沉到规则层**(水印编号、
日期、VLM 提示泄漏、演员标注括号);
2. **LLM 分类层默认关闭**`use_llm` 参数,默认关),只保留规则层;
需要时可按工作流参数显式打开。
保留 884 条而不是 588 条,多出的条目里可能混有水印残余,但**不再误删真对话**。
"""
from __future__ import annotations
import json
from pathlib import Path
from nodes.llm_filter import (
DEFAULT_OVERLAY_TOKENS,
_rule_verdict,
invoke,
parse_srt,
)
from wov_sdk.models import InvokeRequest
WORKSPACE = Path(__file__).resolve().parent.parent
# 真实 OCR 回归数据(1699 行 1666 条),用于量化"规则层不误删对话"。
REAL_OCR = WORKSPACE / "testdata/ocr_srt_run_ac7f480a3ccb.srt"
# ---------------------------------------------------------------------------
# 规则层扩展:原 LLM 层抓到、但可确定性识别的模式
# ---------------------------------------------------------------------------
def test_rule_层删除水印编号() -> None:
"""VLM 把画面水印编号识别成短串(SPHO-1/PHO一号馆/NO.1专用)→ 规则层删除。"""
tokens = set(DEFAULT_OVERLAY_TOKENS)
for text in (
"SPH", "SP10+", "SP10-1型", "SPH-1专用", "SPIO-1 FEB", "NO.1专用",
"PHD-手術", "P10一手机", "PHD一专用", "PH0一1瓶盖", "PHO一号馆",
"SPHO一专用", "SPHO-1", "SPNO-1",
):
assert _rule_verdict(text, tokens) is True, text
# 反例:正常英文/编号样式的真实对白不受影响(含 CJK 或较长英文短语)。
for text in ("青沼君 好可爱", "再见了 再见", "SPA 的感觉真好"):
assert _rule_verdict(text, tokens) is None, text
def test_rule_层删除日期与编号型乱码() -> None:
"""OCR 把画面日期/时间戳识别成条目(2011-11-27、4.0)→ 规则层删除。"""
tokens = set(DEFAULT_OVERLAY_TOKENS)
for text in ("2011-11-27", "2011/11/27", "2011年11月27日", "4.0", "2.0"):
assert _rule_verdict(text, tokens) is True, text
# 反例:含汉字的日期/数值样式不删(可能是真实内容)。
for text in ("10月14岁", "应该有 4.0 就好了"):
assert _rule_verdict(text, tokens) is None, text
def test_rule_层删除VLM提示泄漏() -> None:
"""VLM(glm-ocr)偶尔把系统性提示词回显成识别文本 → 规则层删除。"""
tokens = set(DEFAULT_OVERLAY_TOKENS)
for text in (
"No text is visible in the provided image.",
"The image is blurry and does not contain",
"no text visible",
):
assert _rule_verdict(text, tokens) is True, text
# 反例:正常英文对白(含 CJK 上下文)不受影响。
assert _rule_verdict("谢谢你 松井小姐", tokens) is None
def test_rule_层只删除角色标注_不删除括号内名字() -> None:
"""只删角色标注词((出演));括号内的名字/对白**保留**(避免误删)。
实测数据里(北冈果林)这类演员名括号与"(小声)不要啊"无法用正则可靠
区分,而前者本身是无害字幕文本、删除收益极小,因此宁可全留。
"""
tokens = set(DEFAULT_OVERLAY_TOKENS)
for text in ("(出演)", "(主演)", "(监督)"):
assert _rule_verdict(text, tokens) is True, text
for text in ("(北冈果林)", "(叶山小百合)", "(松井日奈子)", "(我该怎么办)", "(小声)不要啊"):
assert _rule_verdict(text, tokens) is None, text
# ---------------------------------------------------------------------------
# LLM 层默认关闭
# ---------------------------------------------------------------------------
def test_规则层不误删真实对话() -> None:
"""真实数据回归:规则层保留全部含 CJK 的对话条,不误删。
这是本次改动的核心不变式——旧实现(规则+LLM)会删掉 73 条真对话,
新规则层必须一条都不删。用真实 1666 条 OCR 输出验证。
"""
if not REAL_OCR.is_file():
import pytest
pytest.skip("真实 OCR 回归数据缺失")
import re
entries = parse_srt(REAL_OCR.read_text(encoding="utf-8"))
tokens = set(DEFAULT_OVERLAY_TOKENS)
deleted_dialogue = []
for entry in entries:
text = entry["text"]
# 含 >=4 汉字、且不是明显编号/水印形态的,视为真实对话。
if len(re.findall(r"[\u4e00-\u9fff]", text)) >= 4 and not re.match(
r"^(?:98|SPH|SPHO|SPIO|SPNO|SP10|PHO|PH0|PHD|P10|NO\.|\d)", text
):
if _rule_verdict(text, tokens) is True:
deleted_dialogue.append(text)
assert not deleted_dialogue, f"规则层误删真实对话: {deleted_dialogue[:10]}"
def test_invoke_默认不调用LLM(monkeypatch, tmp_path) -> None:
"""默认(不传 use_llm)不调用 LLM:规则层外的一律保留,不做分类判定。"""
calls: list[object] = []
def _boom(*args, **kwargs):
calls.append(args)
raise AssertionError("默认配置不应调用 LLM")
monkeypatch.setattr("urllib.request.urlopen", _boom)
source = tmp_path / "in.srt"
source.write_text(
"1\n00:00:01,000 --> 00:00:02,000\n---\n\n"
"2\n00:00:03,000 --> 00:00:04,000\n好好教育她一番吧\n\n"
"3\n00:00:05,000 --> 00:00:06,000\nSPHO-1\n",
encoding="utf-8",
)
response = invoke(
InvokeRequest(
run_id="r", node_instance_id="", inputs={"srt_uri": str(source)},
output_dir=str(tmp_path / "out"),
)
)
assert response.status == "completed", response.error
assert not calls, "默认不应有任何 LLM 调用"
text = Path(response.outputs["srt_uri"]).read_text(encoding="utf-8")
assert "好好教育她一番吧" in text # 真实对话保留
assert "SPHO-1" not in text # 水印编号由规则层删除
assert response.outputs["kept"] == 1
assert response.outputs["removed"] == 2
def test_invoke_use_llm开启时仍走分类(monkeypatch, tmp_path) -> None:
"""显式 use_llm=1 时保留原 LLM 分类能力(可回退到旧行为)。"""
sent: list[dict] = []
class Response:
def __init__(self):
self._payload = json.dumps(
{"choices": [{"message": {"content": "garbage"}}]}
).encode()
def __enter__(self):
return self
def __exit__(self, *a):
return False
def read(self):
return self._payload
def _open(request, **kwargs):
sent.append(json.loads(request.data))
return Response()
monkeypatch.setattr("urllib.request.urlopen", _open)
source = tmp_path / "in.srt"
source.write_text(
"1\n00:00:01,000 --> 00:00:02,000\n好好教育她一番吧\n",
encoding="utf-8",
)
response = invoke(
InvokeRequest(
run_id="r", node_instance_id="", inputs={"srt_uri": str(source)},
params={"use_llm": "1"}, output_dir=str(tmp_path / "out"),
)
)
assert response.status == "completed", response.error
assert sent, "use_llm=1 时应调用 LLM"
assert response.outputs["kept"] == 0 # LLM 判 garbage → 删除
+3 -1
View File
@@ -41,7 +41,9 @@
"params": { "params": {
"pool_min_workers": 1, "pool_min_workers": 1,
"pool_max_workers": 20, "pool_max_workers": 20,
"pool_fast_threshold": 1 "pool_fast_threshold": 1,
"use_llm": "0",
"_note_use_llm": "0(默认,2026-09 起):只跑确定性规则层,不做 LLM 五类分类。实测(run_ac7f480a3ccb 1666 条真实 OCRLLM 层额外删除的 131 条中 56%(73 条)是真实对话('好好教育她一番吧'/'腿不要合上'/'这家医院 为VIP患者提供了特殊服务'),而它真正抓住而规则层抓不到的仅 58 条且大半已下沉为规则(水印编号/日期/VLM 提示回显/角色标注),repeat 类别 67 条判定零删除——净收益为负。关闭后保留 863 条(旧 588 条)、误删真对话 0 条(旧 73 条)、无 LLM 调用(0.00s vs 52s)。正例:规则层删掉 '---'/'HTML'/'___'/'SPHO-1'/'2011-11-27' 等确定性垃圾,同时保留全部真实对白。反例:设为 1 会把 '差不多想要肉棒了吧'、'应该已经察觉到 至今为止的一切了吧' 等真对话当 garbage 删掉。需要旧行为时置为 1(pool_* 参数随之生效)。"
}, },
"inputs": { "inputs": {
"srt_uri": "ocr.srt_uri" "srt_uri": "ocr.srt_uri"