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>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
"""测试拍照录入在课本词表照片上的识别成功率。"""
|
||||
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()
|
||||
Reference in New Issue
Block a user