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:
john
2026-06-08 15:13:59 +08:00
parent db5465c646
commit 05e173b293
115 changed files with 12709 additions and 1212 deletions
+158
View File
@@ -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()