Files
wordloop/backend/scripts/test_photo_scan_accuracy.py
john 05e173b293 Ship native iOS app, Wiki/TTS backend, and skeleton loading UX.
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>
2026-06-08 15:13:59 +08:00

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