"""测试拍照录入在课本词表照片上的识别成功率。""" 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()