122 lines
4.5 KiB
Python
122 lines
4.5 KiB
Python
#!/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> # 复核全部摘要
|
||
python3 kocr-summary-review-4b.py <file.xls> --limit 30 # 只看前 30 条
|
||
|
||
判断维度(4B 输出 reasons):
|
||
- placeholder: 占位符("会计凭证"/公司名/元数据)
|
||
- truncated: 疑似截断(过短/不完整)
|
||
- suspicious: 与科目明显矛盾 / 含乱码 / 异常字符
|
||
- ok: 真实业务摘要
|
||
|
||
原则:只标记不修改。牧尘铁律——"优先OCR识别为准,不添加后处理逻辑覆盖OCR结果"。
|
||
"""
|
||
import json, os, sys, urllib.request
|
||
try:
|
||
import xlrd
|
||
except ImportError:
|
||
print("需要 xlrd: pip install xlrd")
|
||
sys.exit(1)
|
||
|
||
LLAMA_URL = "https://apihub.agnes-ai.com/v1/chat/completions"
|
||
REVIEW_PROMPT = """你是会计凭证摘要质检员。判断这条凭证摘要是否合格。
|
||
|
||
不合格情况(输出对应标记):
|
||
- placeholder: "会计凭证"/纯公司名/制单审核等元数据占位
|
||
- truncated: 明显截断(如"收费-假日"这种半截、结尾异常)
|
||
- suspicious: 含乱码/特殊字符/与科目矛盾/明显 OCR 错误
|
||
合格: ok
|
||
|
||
凭证摘要: "{summary}"
|
||
科目: {subject}
|
||
|
||
只输出严格 JSON: {"verdict": "ok|placeholder|truncated|suspicious", "reason": "一句话"}"""
|
||
|
||
|
||
def review_one(summary, subject, timeout=45):
|
||
prompt = REVIEW_PROMPT.replace("{summary}", summary[:80]).replace("{subject}", subject[:40])
|
||
payload = json.dumps({
|
||
"model": "agnes-2.0-flash",
|
||
"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": "Bearer sk-7k9e9KGcoZdDuYt2LSA4YXdBTioczleGJm2zzLWCku072ikW", "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()
|
||
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:
|
||
return json.loads(content[start:end + 1])
|
||
except Exception:
|
||
pass
|
||
return {"verdict": "error", "reason": "4B 调用失败"}
|
||
|
||
|
||
def main():
|
||
if len(sys.argv) < 2:
|
||
print("用法: python3 kocr-summary-review-4b.py <合并凭证.xls> [--limit N]")
|
||
return
|
||
path = sys.argv[1]
|
||
limit = 200
|
||
if "--limit" in sys.argv:
|
||
try:
|
||
limit = int(sys.argv[sys.argv.index("--limit") + 1])
|
||
except Exception:
|
||
pass
|
||
|
||
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)
|
||
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()
|