diff --git a/README.md b/README.md index e99eac5..b83fdd7 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ npx promptfoo@0.124.1 eval -c evals/promptfooconfig.quality.yaml - **AI 感阈值是经验值**:`visual_audit.py` 的饱和度、平滑度、死白、边缘密度这些判据,还没用真实的好图 / AI 图批次标定过,当提示用,不当结论 - **语料库只有 2 条种子**:检索质量取决于样本量,现在属于「能用但不够准」 - **`run_loop.py --generate` 未实跑**:需要授权与 API 余额,闭环逻辑只在 `--dry-run` 下验证过 -- **OCR 依赖未装**:图内文字检查在缺 pytesseract 时走 skipped,不会误报通过 +- **OCR 需要本地装 tesseract**:缺 pytesseract 或 tesseract 时这项走 skipped,不会误报通过。装了就能用——脚本会自己扫常见安装路径,默认 tessdata 缺中文时去找用户级 tessdata;CI 环境没装,所以云端不检查图内文字 - **模板覆盖有少量噪声**:7 篇没有围栏代码块的 SOP 用裸段落规则抽取,其中 6 篇各保留 1 段可能偏宽的片段,未逐篇人工复核 ## 许可 diff --git a/tools/check_image.py b/tools/check_image.py index f5c3aa3..65d871f 100644 --- a/tools/check_image.py +++ b/tools/check_image.py @@ -12,10 +12,12 @@ 退出码:0 无 fail;1 有 fail(--strict 时 warn 也算失败);2 缺 Pillow,只能做结构检查。 仅依赖 Pillow;OCR 另需 pytesseract 与 tesseract,缺失时明确标 skipped,不算通过。 +tesseract 不在 PATH 时脚本会自己扫常见安装路径;默认 tessdata 缺中文时去找用户级 tessdata。 """ import argparse import json import os +import shutil import struct import sys import warnings @@ -130,6 +132,53 @@ def corner_background(small): return (_median(p[0] for p in pixels), _median(p[1] for p in pixels), _median(p[2] for p in pixels)) +TESSERACT_CANDIDATES = ( + r"C:\Program Files\Tesseract-OCR\tesseract.exe", + r"C:\Program Files (x86)\Tesseract-OCR\tesseract.exe", + r"%LOCALAPPDATA%\Programs\Tesseract-OCR\tesseract.exe", + "/usr/bin/tesseract", + "/usr/local/bin/tesseract", + "/opt/homebrew/bin/tesseract", +) + +TESSDATA_CANDIDATES = ( + r"%LOCALAPPDATA%\tesseract\tessdata", + r"%APPDATA%\tesseract\tessdata", + "/usr/local/share/tessdata", + "/opt/homebrew/share/tessdata", +) + + +def resolve_ocr_env(): + """pytesseract 只认 PATH 与 tesseract 自带 tessdata,Windows 装机版常两处都不到位。 + + 这里做一次兜底解析:先 PATH,再扫常见安装路径;默认 tessdata 缺中文时, + 去找用户级的带中文 tessdata。显式设过 TESSDATA_PREFIX 就不覆盖。 + 返回 (二进制路径, tessdata 目录或 None);二进制找不到时返回 (None, None)。 + """ + exe = shutil.which("tesseract") + if not exe: + for cand in TESSERACT_CANDIDATES: + path = os.path.expandvars(cand) + if os.path.isfile(path): + exe = path + break + if not exe: + return None, None + + if os.environ.get("TESSDATA_PREFIX"): + return exe, None + + default_tessdata = os.path.join(os.path.dirname(exe), "tessdata") + if os.path.isfile(os.path.join(default_tessdata, "chi_sim.traineddata")): + return exe, None + for cand in TESSDATA_CANDIDATES: + path = os.path.expandvars(cand) + if os.path.isfile(os.path.join(path, "chi_sim.traineddata")): + return exe, path + return exe, None + + def analyze(path, spec, white_tol=None): """返回 checks 列表与 metrics。Pillow 缺失时只给结构项。""" w, h, fmt = probe_size(path) or (0, 0, "UNKNOWN") @@ -262,6 +311,13 @@ def add(cid, status, detail, hint=""): try: import pytesseract + exe, tessdata = resolve_ocr_env() + if not exe: + raise ImportError("tesseract 二进制未找到") + pytesseract.pytesseract.tesseract_cmd = exe + if tessdata: + os.environ["TESSDATA_PREFIX"] = tessdata + text = pytesseract.image_to_string(im, lang="chi_sim+eng").strip() visible = [line for line in text.splitlines() if line.strip() and len(line.strip()) > 1] ocr = {"status": "done", "lines": len(visible), "sample": visible[:3]} @@ -274,6 +330,9 @@ def add(cid, status, detail, hint=""): add("text", "pass", "未检出图内文字") except ImportError: add("text", "skipped", "OCR 不可用,图内文字未检查", "pip install pytesseract 并安装 tesseract;skipped 不等于通过") + except Exception as exc: + ocr = {"status": "skipped", "reason": f"OCR 执行失败: {exc}"} + add("text", "skipped", f"OCR 执行失败:{exc}", "检查 tesseract 与 chi_sim+eng 语言包是否可用") metrics["ocr"] = ocr return checks, metrics