wxocr / app.py
3v324v23's picture
feat: implement FastAPI-based WeChat OCR service with token authentication, LRU caching, and structured text formatting
c073746
Raw
History Blame Contribute Delete
8.2 kB
import os
import hashlib
import tempfile
import threading
import traceback
from collections import OrderedDict
from fastapi import FastAPI, UploadFile, File, HTTPException, Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
import wcocr
app = FastAPI(title="WeChat OCR API", version="1.0.0")
# 全局变量
ocr_cache = None
ocr_lock = None
API_TOKEN = os.getenv("TOKEN", "my-secret-token")
# 不需要验证的路径
EXCLUDED_PATHS = ["/health"]
class TokenAuthMiddleware(BaseHTTPMiddleware):
"""简单的Token验证中间件"""
async def dispatch(self, request: Request, call_next):
# 跳过不需要验证的路径
if request.url.path in EXCLUDED_PATHS:
return await call_next(request)
# 从请求头获取 Authorization
auth_header = request.headers.get("Authorization")
# 兼容 Bearer 前缀
if auth_header and auth_header.startswith("Bearer "):
auth_header = auth_header[7:] # 移除 "Bearer " 前缀
# 验证token是否匹配
if auth_header != API_TOKEN:
return JSONResponse(
status_code=401,
content={"detail": "未授权:无效的认证令牌"}
)
# token验证通过,继续处理请求
response = await call_next(request)
return response
# 添加中间件
app.add_middleware(TokenAuthMiddleware)
class LRUCache:
"""简单的 LRU 缓存,防止内存溢出"""
def __init__(self, capacity: int = 100):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key):
if key not in self.cache:
return None
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False)
@app.on_event("startup")
async def startup_event():
global ocr_cache, ocr_lock
print("====== 开始初始化 WeChat OCR ======")
try:
# 初始化 WeChat OCR
WECHAT_PATH = os.environ.get("WECHAT_PATH", "/code/wechat")
WXOCR_PATH = os.environ.get("WXOCR_PATH", "/code/wechat/wxocr")
wcocr.init(WXOCR_PATH, WECHAT_PATH)
# 全局缓存对象 (缓存 100 张最近的图片结果)
ocr_cache = LRUCache(capacity=100)
# OCR 引擎并发锁
ocr_lock = threading.Lock()
print("====== 🎉 WeChat OCR 初始化成功,API 已就绪! ======")
except Exception as e:
print("❌ 初始化失败, 错误如下:\n", str(e))
@app.get("/health")
def health_check():
return {"status": "healthy"}
def process_ocr(file_bytes: bytes) -> dict:
"""处理图片并执行 OCR,包含缓存机制"""
# 计算图片 Hash 用于缓存 Key
img_hash = hashlib.md5(file_bytes).hexdigest()
cached_result = ocr_cache.get(img_hash) if ocr_cache else None
if cached_result is not None:
return cached_result
if not file_bytes or len(file_bytes) < 8:
return {"errcode": 400, "errmsg": "Empty or invalid file content"}
# 使用轻量级的 Magic Bytes(文件头魔数)检查代替 cv2,判断是否是常见图片格式
header = file_bytes[:12]
is_image = (
header.startswith(b'\x89PNG\r\n\x1a\n') or # PNG
header.startswith(b'\xff\xd8\xff') or # JPEG
header.startswith(b'BM') or # BMP
header.startswith(b'GIF8') or # GIF
(header.startswith(b'RIFF') and header[8:12] == b'WEBP') # WEBP
)
if not is_image:
return {"errcode": 400, "errmsg": "Unsupported or invalid image format"}
# 写入临时文件供 wcocr 调用
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_file:
tmp_file.write(file_bytes)
tmp_file.flush()
tmp_path = tmp_file.name
try:
# 加锁执行 OCR,防止底层组件崩溃
if ocr_lock:
with ocr_lock:
result = wcocr.ocr(tmp_path)
else:
result = wcocr.ocr(tmp_path)
finally:
# 清理临时文件
if os.path.exists(tmp_path):
os.remove(tmp_path)
# 存入缓存
if ocr_cache:
ocr_cache.put(img_hash, result)
return result
def format_structured_text(ocr_response: list) -> str:
"""基于坐标的文本行合并算法,还原原始文本的换行结构"""
if not ocr_response:
return ""
# 按 Y 轴 (top) 排序
boxes = sorted(ocr_response, key=lambda x: x['top'])
lines = []
current_line = []
for box in boxes:
if not current_line:
current_line.append(box)
continue
# 计算当前行的中心基线
line_top_avg = sum(b['top'] for b in current_line) / len(current_line)
line_bottom_avg = sum(b['bottom']
for b in current_line) / len(current_line)
line_center = (line_top_avg + line_bottom_avg) / 2
# 如果当前 box 的 top 小于上一行的中心点,认为在同一行
if box['top'] < line_center:
current_line.append(box)
else:
lines.append(current_line)
current_line = [box]
if current_line:
lines.append(current_line)
# 将同一行的文本按 X 轴 (left) 排序并拼接
structured_text = []
for line in lines:
line.sort(key=lambda x: x['left'])
line_text = " ".join([b['text'] for b in line])
structured_text.append(line_text)
return "\n".join(structured_text)
# ==========================================
# 接口 1:OCR 识别后的原始结构
# ==========================================
@app.post("/api/ocr/raw")
async def api_ocr_raw(file: UploadFile = File(...)):
try:
contents = await file.read()
raw_result = process_ocr(contents)
if raw_result.get('errcode', 0) != 0:
return JSONResponse(status_code=400, content=raw_result)
return raw_result
except Exception as e:
print(f"❌ [api_ocr_raw] 发生异常: {str(e)}")
traceback.print_exc()
raise HTTPException(
status_code=500, detail=f"OCR raw extraction error: {str(e)}")
# ==========================================
# 接口 2:识别后的纯文本 (无分隔符直接拼接)
# ==========================================
@app.post("/api/ocr/text")
async def api_ocr_text(file: UploadFile = File(...)):
try:
contents = await file.read()
raw_result = process_ocr(contents)
if raw_result.get('errcode', 0) != 0:
return JSONResponse(status_code=400, content=raw_result)
ocr_response = raw_result.get('ocr_response', [])
pure_text = "".join([item['text'] for item in ocr_response])
return {
"errcode": 0,
"text": pure_text
}
except Exception as e:
print(f"❌ [api_ocr_text] 发生异常: {str(e)}")
traceback.print_exc()
raise HTTPException(
status_code=500, detail=f"OCR text extraction error: {str(e)}")
# ==========================================
# 接口 3:识别后的纯文本,保留原始结构换行
# ==========================================
@app.post("/api/ocr/structured")
async def api_ocr_structured(file: UploadFile = File(...)):
try:
contents = await file.read()
raw_result = process_ocr(contents)
if raw_result.get('errcode', 0) != 0:
return JSONResponse(status_code=400, content=raw_result)
ocr_response = raw_result.get('ocr_response', [])
structured_text = format_structured_text(ocr_response)
return {
"errcode": 0,
"text": structured_text
}
except Exception as e:
print(f"❌ [api_ocr_structured] 发生异常: {str(e)}")
traceback.print_exc()
raise HTTPException(
status_code=500, detail=f"OCR structured extraction error: {str(e)}")