🤖 本文由 OpenClaw + DeepSeek 自动生成

一次从远程 OCR 降级到本地 Tesseract 的架构痛史,顺便干掉三个隐蔽 Bug。


背景

我平时跑步用 Nike Run Club(NRC),跑完后截个图发到自己的 Telegram Bot,Bot 会自动把图片存入服务器。我有一个后台 Watcher 脚本,专门监控这个图片目录,一旦发现新图片就 OCR 提取运动数据(距离、时长、配速、卡路里),然后自动归档到 Notion 的 Sports 数据库,实现"跑完截个图,一切自动归档"的极简体验。

本来这套流程跑得好好的,直到前天,突然崩了。

问题现象

截图发过去,Bot 这边显示图片已保存,但 Notion 里就是没有新记录。手动跑脚本测试,发现 OCR 一直超时,偶尔跑出来也是空结果。

开始排查。


Bug 1:远程 OCR 的链路灾难

起初 Watcher 的 OCR 链路是这样的:图片 → image 工具调用外部视觉 API → 返回文本。这套链路的核心问题在于:

  1. 网络链路绕转:图片先被传送到远程 AI 推理服务器,等返回结果再回传
  2. image 工具不支持 /tmp 路径:Watcher 处理的图片在 /tmp 临时目录,正好是 image 工具不支持读取的路径
  3. 链路过长:每张图都要走一遍 本地 → 远程 API → 解析 → 返回 的完整往返

一张截图 OCR 要等 30 秒以上,频繁超时。

解决:彻底抛弃远程视觉 API,改用纯本地 OCR 链路。

第一步:写了一个预处理脚本,对 Nike 跑步截图(深色背景 + 白色文字)做反色预处理,把白字黑底变成黑字白底,PIL 一行搞定:

img_inv = ImageOps.invert(img.convert('RGB'))

第二步:直接用 pytesseract(本地 Tesseract OCR 引擎)做文字识别:

text = pytesseract.image_to_string(img_inv, lang='eng', config='--psm 6')

结果:3 秒出结果,零外部依赖,零网络开销。


Bug 2:被忽略的双 OCR 处理

修复完 Bug 1 之后,重新跑了一遍,发现每张图还是卡了 6-7 秒。

仔细一看代码——原来老的 Watcher 脚本里,在预处理阶段就已经对原图做了一次 OCR,后续逻辑里又调了一次 OCR,同一张图 OCR 了两遍。翻倍耗时,纯纯的 Bug。

解决:删掉预处理的 OCR 调用,单次反色预处理(1.7 秒)→ 单次 Tesseract OCR(1.3 秒),全程 3 秒完成。


Bug 3:函数重名引发的逻辑静默失效

这是最隐蔽的一个 Bug。

Watcher 脚本里有两个地方都定义了叫 is_run_screenshot 的函数——一个在老代码的轮询逻辑里,另一个在 inotify 响应逻辑里。问题是,按照 Python 的解析规则,后定义的函数完全覆盖了先定义的,而第二个定义恰恰是一个空壳实现,永远返回 False

所以无论发什么截图过去,脚本的逻辑判断都是"非跑步图片,跳过",但没有任何报错——静默失效。

# 第一个定义(轮询模式)—— 有完整逻辑
def is_run_screenshot(text):
    if has_duration and (has_pace or has_decimal):
        return True
    ...

# 第二个定义(inotify 模式)—— 空壳,覆盖了第一个
def is_run_screenshot(text):
    return False  # 静默失效

解决:删掉第二个空壳定义,确保只有一个 is_run_screenshot 函数生效。同时将识别逻辑改为更宽松的版本:

has_duration = bool(re.search(r'\d+:\d+:\d+', text))
has_decimal = bool(re.search(r'\d+\.\d+', text))
has_pace = bool(re.search(r"\d+'\d+", text))
if has_duration and (has_pace or has_decimal):
    return True

Bug 4:轮询间隔 3 秒的滞后感

原来的 Watcher 使用 time.sleep(3) 轮询目录,每次循环检查所有文件。从截图落盘到 OCR 完成,平均要等 5-8 秒。

虽然不影响功能完整性,但在 TG 上发完截图后等几秒才有反馈,体验不够流畅。

解决:引入 watchdog 库的 inotify 机制,改为事件驱动:

from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class InotifyHandler(FileSystemEventHandler):
    def on_created(self, event):
        time.sleep(0.5)  # 等文件写完成
        process_image(path)

实测效果:事件触发 < 0.5 秒,截图刚在 TG 上显示"已接收",Watcher 就启动 OCR 了。


最终架构与演进总结

技术栈

组件 说明
OCR 引擎 Tesseract + pytesseract(本地)
预处理 PIL / ImageOps(反色处理)
文件监控 watchdog.inotify
归档目标 Notion Sports 数据库
运行方式 nohup 后台常驻进程

踩坑清单

  1. 远程 OCR 链路脆弱:网络抖动、路径限制、超时风险 → 本地 OCR 是唯一正解
  2. 同图双 OCR:预处理阶段的遗留逻辑与主逻辑冲突 → 单次处理原则
  3. 函数重名覆盖:Python 同名函数后覆盖前,无任何警告 → grep 搜索函数名做一次全文件审计
  4. 轮询 vs 事件驱动:3 秒轮询在体验上差了一个量级 → 能用事件就别轮询

当前 Watcher 运行状态

# 进程状态
ps aux | grep run_watcher
openclaw  1848607  ...  python3 run_watcher.py

# 日志位置
/tmp/run_watcher.log

# cron @reboot 已添加,重启自动拉起

延伸思考

这次 debug 最值得记住的是 Bug 3:函数重名导致的静默失效。没有报错、没有异常日志,逻辑直接被覆盖成空壳,但脚本一直在正常运行——它只是假装在工作而已。

这类 Bug 比显式崩溃更危险:你以为系统在正常工作,实际上它在假装工作。

从那以后我给自己的所有 Python 项目加了一条规则:项目中不允许出现两个同名的顶层函数定义,CI 脚本里加一行 grep -n "^def is_" *.py | sort 检查全部函数签名。


归档时间:2026-06-05 GMT+8
作者:Longfortt
关联文章《OpenClaw + 私人中台优化实录》