05e173b293
Replace the WebView shell with SwiftUI screens, add account-scoped Wiki and TTS APIs with adaptive review and photo scan support, and keep web/iOS pages usable while data loads asynchronously. Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
4.8 KiB
Python
159 lines
4.8 KiB
Python
"""测试拍照录入在课本词表照片上的识别成功率。"""
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
BACKEND_DIR = Path(__file__).resolve().parent.parent
|
||
sys.path.insert(0, str(BACKEND_DIR))
|
||
|
||
from services.word_scan_service import word_scan_service # noqa: E402
|
||
|
||
IMAGE = Path(
|
||
"/Users/john/.cursor/projects/Users-john-PycharmProjects-wordloop/assets/"
|
||
"bf2e15b4e1a36836efb4e9a72b200d8e-f357a710-6d8d-4f5b-a52c-0bea4420aada.png"
|
||
)
|
||
|
||
GROUND_TRUTH: list[tuple[str, str]] = [
|
||
("feed the cows", "喂奶牛"),
|
||
("see a film", "看电影"),
|
||
("visit Grandma and Grandpa", "看望爷爷奶奶"),
|
||
("have a good time", "过得愉快"),
|
||
("do the housework", "做家务"),
|
||
("tidy my room", "整理我的房间"),
|
||
("make the bed", "铺床"),
|
||
("dish", "碟;盘"),
|
||
("happy", "高兴的"),
|
||
("excited", "激动的;兴奋的"),
|
||
("upset", "难过的"),
|
||
("angry", "愤怒的;生气的"),
|
||
("play music", "放音乐"),
|
||
("sing and dance", "唱歌跳舞"),
|
||
("talk with the baby", "和婴儿说话"),
|
||
("play a game", "玩游戏"),
|
||
("share", "分享"),
|
||
("have a go", "尝试"),
|
||
("nice", "令人愉快的;吸引人的"),
|
||
("cook", "厨师"),
|
||
("great", "伟大的"),
|
||
("teacher", "教师"),
|
||
("tree", "树"),
|
||
("leaf (pl. leaves)", "叶子"),
|
||
("flower", "花"),
|
||
("grass", "草"),
|
||
("duck", "鸭"),
|
||
("pond", "池塘;水池"),
|
||
("frog", "青蛙"),
|
||
("lake", "湖"),
|
||
("fish", "鱼"),
|
||
("river", "河;江"),
|
||
("shark", "鲨鱼"),
|
||
("sea", "海;海洋"),
|
||
("wash my hands", "(我)洗手"),
|
||
("before eating", "吃东西前"),
|
||
("after using the toilet", "上厕所后"),
|
||
("dirty", "脏的"),
|
||
("clean", "干净的"),
|
||
("drink some milk", "喝一些牛奶"),
|
||
("say good night", "道晚安"),
|
||
("sleep", "睡觉;入睡"),
|
||
("read a story", "读故事"),
|
||
]
|
||
|
||
|
||
def normalize_en(text: str) -> str:
|
||
return re.sub(r"\s+", " ", text.strip().lower())
|
||
|
||
|
||
def normalize_zh(text: str) -> str:
|
||
return re.sub(r"\s+", "", text.strip())
|
||
|
||
|
||
def fuzzy_zh_match(pred: str, truth: str) -> bool:
|
||
p = normalize_zh(pred)
|
||
t = normalize_zh(truth)
|
||
if not p or not t:
|
||
return False
|
||
if p == t:
|
||
return True
|
||
# 允许 OCR/截断导致的中文释义部分匹配(>=70% 字符重合)
|
||
common = sum(1 for ch in t if ch in p)
|
||
return common / max(len(t), 1) >= 0.7
|
||
|
||
|
||
def fuzzy_en_match(pred: str, truth: str) -> bool:
|
||
p = normalize_en(pred)
|
||
t = normalize_en(truth)
|
||
if p == t:
|
||
return True
|
||
# 短语轻微 OCR 错误:去掉空格后前缀匹配
|
||
p2 = re.sub(r"[^a-z()\.]", "", p)
|
||
t2 = re.sub(r"[^a-z()\.]", "", t)
|
||
if not p2 or not t2:
|
||
return False
|
||
return t2 in p2 or p2 in t2 or p2.startswith(t2[: max(4, len(t2) - 2)])
|
||
|
||
|
||
def run_tesseract(image_path: Path, psm: str = "6") -> str:
|
||
result = subprocess.run(
|
||
["tesseract", str(image_path), "stdout", "-l", "chi_sim+eng", "--psm", psm],
|
||
capture_output=True,
|
||
text=True,
|
||
check=False,
|
||
)
|
||
return result.stdout or ""
|
||
|
||
|
||
def evaluate(items: list[dict[str, str]], label: str) -> dict:
|
||
preds = [(i["source_text"], i["target_text"]) for i in items]
|
||
matched = []
|
||
missed = []
|
||
for en, zh in GROUND_TRUTH:
|
||
hit = any(fuzzy_en_match(p_en, en) and fuzzy_zh_match(p_zh, zh) for p_en, p_zh in preds)
|
||
if hit:
|
||
matched.append((en, zh))
|
||
else:
|
||
missed.append((en, zh))
|
||
|
||
recall = len(matched) / len(GROUND_TRUTH)
|
||
precision = len(matched) / max(len(preds), 1)
|
||
print(f"\n=== {label} ===")
|
||
print(f"标准词条: {len(GROUND_TRUTH)}")
|
||
print(f"识别词条: {len(preds)}")
|
||
print(f"命中: {len(matched)} 漏识: {len(missed)}")
|
||
print(f"召回率(成功率): {recall * 100:.1f}%")
|
||
print(f"精确率: {precision * 100:.1f}%")
|
||
if missed:
|
||
print("漏识样例:", ", ".join(f"{en}" for en, _ in missed[:12]))
|
||
if len(missed) > 12:
|
||
print(f"... 另有 {len(missed) - 12} 条")
|
||
return {"recall": recall, "matched": len(matched), "pred_count": len(preds), "missed": missed}
|
||
|
||
|
||
def main() -> None:
|
||
if not IMAGE.exists():
|
||
print(f"图片不存在: {IMAGE}")
|
||
sys.exit(1)
|
||
|
||
ocr_raw = run_tesseract(IMAGE)
|
||
print("--- OCR 原文(前 1200 字)---")
|
||
print(ocr_raw[:1200])
|
||
|
||
try:
|
||
items = word_scan_service.parse_scan_text(ocr_raw)
|
||
result = evaluate(items, "当前流水线: 原始 OCR + 解析")
|
||
except Exception as exc:
|
||
print(f"\n解析失败: {exc}")
|
||
result = {"recall": 0.0}
|
||
|
||
passed = result["recall"] >= 0.9
|
||
print(f"\n结论: {'达到' if passed else '未达到'} 90% 成功率目标(召回 {result['recall']*100:.1f}%)")
|
||
sys.exit(0 if passed else 1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|