框架: clean抗性三连——detect单窗失败降级备选模型不断书+apply书名水印豁免+水印裁剪防正文前缀误删(路C实测病对症)

This commit is contained in:
zizi 2026-07-13 15:29:05 +08:00
parent a18db4bb01
commit 0a3a576704
2 changed files with 51 additions and 15 deletions

View File

@ -35,6 +35,10 @@ GARBAGE_LINE_PAT = re.compile(
r"|手打中|稍等片刻|重新刷新|获取最新", # 盗版站占位残留1040-1060 验证窗实测)
re.I)
# 水印裁剪:盗版源把水印粘在正文行尾(如『“我草!”百镀一下“××”最新章节第一时间免费阅读。』),
# LLM 会把整行报上来——删除段若含此类明确水印且水印前有前缀(多为正文),只删水印部分。
WATERMARK_PAT = re.compile(r"(?:百镀一下|百度一下|一秒记住).{0,45}?(?:免费阅读。?|免费读!?|阅读。)")
def flex_pattern(exact):
"""空白弹性正则:非空白文字逐字 escape连续空白段 → \\s+。
@ -92,6 +96,10 @@ def main(work_id, batch, file_, model, dry_run, report_md):
targets = sorted({n for no in by_ch for n in (no - 1, no, no + 1) if n >= 1})
stats = {"删除段": 0, "删除字数": 0, "拒绝": [], "弹性命中": 0, "邻章命中": 0}
with psycopg.connect(DSN) as conn:
# 书名水印豁免:盗版源每章插孤立书名行——孤行精确等于本书书名不可能是正文叙述
work_title = (conn.execute(
"SELECT title FROM muse_content_work WHERE tenant_id=%s AND id=%s",
(TENANT, work_id)).fetchone() or ("",))[0]
rows = conn.execute(
"""SELECT c.order_no, c.id, c.title, b.id, b.content_text
FROM muse_content_chapter c JOIN muse_content_block b ON b.chapter_id=c.id AND b.deleted=FALSE
@ -110,9 +118,11 @@ def main(work_id, batch, file_, model, dry_run, report_md):
if not (MIN_LEN <= len(exact) <= MAX_LEN):
stats["拒绝"].append(f"#{no}: 长度越界({len(exact)}) {exact[:24]!r}")
continue
# 守卫4行级垃圾特征——无特征的行=疑混正文
# 守卫4行级垃圾特征——无特征的行=疑混正文(孤立书名行水印豁免)
lines = [ln for ln in exact.split("\n") if ln.strip()]
plain = [ln for ln in lines if not GARBAGE_LINE_PAT.search(ln)]
plain = [ln for ln in lines
if not GARBAGE_LINE_PAT.search(ln)
and not (work_title and ln.strip() == work_title)]
if plain and len(lines) == 1:
stats["拒绝"].append(f"#{no}: 无垃圾特征疑正文 {exact[:24]!r}")
continue
@ -121,10 +131,17 @@ def main(work_id, batch, file_, model, dry_run, report_md):
for ln in plain:
stats["拒绝"].append(f"#{no}: 拆行跳过疑正文行 {ln[:24]!r}")
units = [(ln, reason + "(拆行)") for ln in lines
if GARBAGE_LINE_PAT.search(ln) and MIN_LEN <= len(ln) <= MAX_LEN]
if (GARBAGE_LINE_PAT.search(ln)
or (work_title and ln.strip() == work_title))
and MIN_LEN <= len(ln) <= MAX_LEN]
else:
units = [(exact, reason)]
for u_exact, u_reason in units:
# 水印裁剪水印前的前缀多为正文不删exact 缩到水印起点
wm = WATERMARK_PAT.search(u_exact)
if wm and wm.start() > 0:
u_exact = u_exact[wm.start():]
u_reason += "(水印裁剪)"
# 本章两级匹配 → 邻章容错(章号偏移是 LLM 在长窗里的已知病)
hit_no = None
for cand in (no, no - 1, no + 1):

View File

@ -45,13 +45,15 @@ PROMPT = """你是网文正文清洗探测器。下面是《{title}》第 {a}
@click.option("--work-id", type=int, required=True)
@click.option("--win", "wins", type=int, multiple=True, help="指定窗号(可多次);不给则全部窗")
@click.option("--model", default="MiniMax-M3", show_default=True)
@click.option("--fallback-model", default="deepseek-v4-flash", show_default=True,
help="主模型失败(如上游敏感词拦截)时的备选;再失败则记录跳过该窗")
@click.option("--force", is_flag=True, help="已有产物也重跑")
def main(work_id, wins, model, force):
def main(work_id, wins, model, fallback_model, force):
d = OUT / str(work_id)
manifest = json.loads((d / "manifest.json").read_text())
title = manifest["title"]
targets = [m for m in manifest["windows"] if not wins or m["win"] in wins]
total_in = total_out = n_del = 0
total_in = total_out = n_del = n_skip = 0
for m in targets:
f_out = d / f"deletions-{m['win']:03d}.json"
if f_out.exists() and not force:
@ -59,18 +61,35 @@ def main(work_id, wins, model, force):
continue
text = pathlib.Path(m["file"]).read_text()
t0 = time.time()
content, usage = chat(
PROMPT.format(title=title, a=m["from"], b=m["to"], text=text),
model=model)
prompt = PROMPT.format(title=title, a=m["from"], b=m["to"], text=text)
# 单窗失败不中断整书:主模型 → 备选模型 → 记录跳过(网文正文可能触发上游敏感词拦截)
dels, used_model = None, model
for cand in (model, fallback_model):
try:
content, usage = chat(prompt, model=cand)
data = extract_json(content)
dels = data.get("deletions", data if isinstance(data, list) else [])
f_out.write_text(json.dumps({"deletions": dels}, ensure_ascii=False, indent=1))
used_model = cand
total_in += usage.get("prompt_tokens", 0)
total_out += usage.get("completion_tokens", 0)
break
except (RuntimeError, ValueError) as e:
click.echo(f"win-{m['win']:03d} {cand} 失败: {str(e)[:120]}", err=True)
if dels is None:
# 双模型全败写空产物占位含跳过原因apply 端窗产物齐备可继续,审计可追
f_out.write_text(json.dumps(
{"deletions": [], "skipped": "主/备模型均失败(多为上游敏感词拦截)"},
ensure_ascii=False, indent=1))
n_skip += 1
click.echo(f"win-{m['win']:03d}(第{m['from']}{m['to']}章)探测跳过,该窗不清洗")
continue
f_out.write_text(json.dumps({"deletions": dels, "model": used_model},
ensure_ascii=False, indent=1))
n_del += len(dels)
click.echo(f"win-{m['win']:03d}(第{m['from']}{m['to']}章)检出 {len(dels)}"
tag = "" if used_model == model else f"(备选 {used_model}"
click.echo(f"win-{m['win']:03d}(第{m['from']}{m['to']}章)检出 {len(dels)}{tag} "
f"{f_out.name}{time.time() - t0:.0f}s")
click.echo(f"探测完成:{len(targets)} 窗共检出 {n_del} 段;"
click.echo(f"探测完成:{len(targets)} 窗共检出 {n_del}(跳过 {n_skip} 窗)"
f"token in={total_in:,} out={total_out:,}")