xiaowei-system/scripts/kocr-summary-review-4b.py

180 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
kocr-summary-review-4b.py — KOCR 输出摘要质量复核2026-09-06 P2
=================================================================
⚠️ 独立复核工具:不接入 KOCR 自动管线(管线 v9.4 冻结,禁止覆盖 OCR 结果)。
用法:读已生成的合并凭证.xls → 本地 4B 逐条判断摘要质量 → 输出可疑清单(纯文本供人工复核)。
用法:
python3 kocr-summary-review-4b.py <合并凭证.xls> # 复核全部摘要2B 本地免费优先)
python3 kocr-summary-review-4b.py <file.xls> --limit 30 # 只看前 30 条
python3 kocr-summary-review-4b.py <file.xls> --cloud # 强制 agnes 云复核(质量敏感)
判断维度4B 输出 reasons:
- placeholder: 占位符("会计凭证"/公司名/元数据)
- truncated: 疑似截断(过短/不完整)
- suspicious: 与科目明显矛盾 / 含乱码 / 异常字符
- ok: 真实业务摘要
原则:只标记不修改。牧尘铁律——"优先OCR识别为准不添加后处理逻辑覆盖OCR结果"
"""
import json, os, sys, urllib.request
def _gateway_key():
"""网关 key 从 config.yaml providers.newapi-local 读(不写死在脚本里)"""
try:
import yaml
cfg = yaml.safe_load(open(os.path.expanduser("~/.hermes/config.yaml")))
return (cfg.get("providers") or {}).get("newapi-local", {}).get("api_key", "")
except Exception:
return os.environ.get("NEWAPI_KEY", "")
try:
import xlrd
except ImportError:
print("需要 xlrd: pip install xlrd")
sys.exit(1)
# 模型链2B 本地(免费) → agnes(付费复核)。质量敏感可用 --cloud 强制云
LOCAL_URL = "http://127.0.0.1:8080/v1/chat/completions"
LOCAL_MODEL = "minicpm5-2b"
LLAMA_URL = "http://127.0.0.1:3000/v1/chat/completions" # 统一走 newapi
AGNES_MODEL = "agnes-2.5-flash"
REVIEW_PROMPT = """你是会计凭证摘要质检员。判断这条凭证摘要是否合格。
不合格情况(输出对应标记):
- placeholder: "会计凭证"/纯公司名/制单审核等元数据占位
- truncated: 明显截断(如"收费-假日"这种半截、结尾异常)
- suspicious: 含乱码/特殊字符/与科目矛盾/明显 OCR 错误
合格: ok
凭证摘要: "{summary}"
科目: {subject}
只输出严格 JSON: {"verdict": "ok|placeholder|truncated|suspicious", "reason": "一句话"}"""
def _agnes_key():
return _gateway_key()
"""从 .env / config 读 agnes key"""
try:
with open(os.path.expanduser("~/.hermes/.env")) as f:
for line in f:
if "AGNES_API_KEY" in line:
return line.strip().split("=", 1)[1].strip()
except Exception:
pass
return ""
def review_one(summary, subject, use_cloud=False, timeout=45):
"""调 LLM 判断摘要质量。默认 2B 本地免费use_cloud=True 强制 agnes。
"""
prompt = REVIEW_PROMPT.replace("{summary}", summary[:80]).replace("{subject}", subject[:40])
if not use_cloud:
# 先试本地 2B
try:
payload = json.dumps({
"model": LOCAL_MODEL,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 60, "temperature": 0.1,
}).encode()
req = urllib.request.Request(
LOCAL_URL, data=payload,
headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = json.loads(resp.read())
content = (body.get("choices") or [{}])[0].get("message", {}).get("content", "")
if content.strip():
return _parse_verdict(content)
except Exception:
pass # 2B 不可用 → 云
# agnes 云(付费复核)
key = _agnes_key()
payload = json.dumps({
"model": AGNES_MODEL,
"messages": [{"role": "system", "content": prompt},
{"role": "user", "content": "请判断"}],
"max_tokens": 60, "temperature": 0.1,
}).encode()
req = urllib.request.Request(
LLAMA_URL, data=payload,
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
method="POST")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = json.loads(resp.read())
content = (body.get("choices") or [{}])[0].get("message", {}).get("content", "")
content = content.strip()
return _parse_verdict(content)
except Exception:
pass
return {"verdict": "error", "reason": "复核调用失败"}
def _parse_verdict(content):
content = content.strip()
if content.startswith("```"):
content = content.split("```")[1] if "```" in content[3:] else content
if content.startswith("json"):
content = content[4:]
start, end = content.find("{"), content.rfind("}")
if start >= 0 and end > start:
try:
return json.loads(content[start:end + 1])
except Exception:
pass
return {"verdict": "error", "reason": "输出解析失败"}
def main():
if len(sys.argv) < 2:
print("用法: python3 kocr-summary-review-4b.py <合并凭证.xls> [--limit N]")
return
path = sys.argv[1]
limit = 200
use_cloud = False
args = sys.argv[2:]
if "--limit" in args:
try:
limit = int(args[args.index("--limit") + 1])
except Exception:
pass
if "--cloud" in args:
use_cloud = True # 质量敏感时强制云复核
wb = xlrd.open_workbook(path)
ws = wb.sheet_by_name("Page1")
# 找列: 凭证摘要 / 科目名称 / 科目代码 / 凭证号
headers = [str(ws.cell_value(0, c)).strip() for c in range(ws.ncols)]
def col(keyword):
for c, h in enumerate(headers):
if keyword in h:
return c
return None
ci_sum, ci_subj, ci_code, ci_vno = col("摘要"), col("科目名称"), col("科目代码"), col("凭证号")
issues = []
checked = 0
for r in range(1, ws.nrows):
summary = str(ws.cell_value(r, ci_sum or 19)).strip()
if not summary:
continue
checked += 1
if checked > limit:
break
subject = str(ws.cell_value(r, ci_subj or 6)).strip()
vno = str(ws.cell_value(r, ci_vno or 4)).strip()
res = review_one(summary, subject, use_cloud=use_cloud)
if res.get("verdict") != "ok":
issues.append(f"记-{vno} | {res.get('verdict')} | {summary[:60]} | 科目:{subject[:20]} | {res.get('reason','')[:40]}")
print(f"复核 {checked} 条摘要, 可疑 {len(issues)} 条:")
for line in issues:
print(line)
if not issues:
print("✅ 全部摘要合格4B 复核)")
if __name__ == "__main__":
main()