serve_lang_judge.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 语种/方向判断 sidecar 服务(子系统 C)—— PaddleOCR 常驻 HTTP 服务
  5. 用途:
  6. 主识别接口 serve_recognition_api.py 运行在 pytorch 环境,无法 import PaddleOCR
  7. (paddleocr==3.2.0 与 pytorch 环境的包互不兼容,见框架文档第三之二节)。
  8. 故把「OCR + 语种判定 + 方向角」拆成独立进程,跑在 paddleocr conda 环境,
  9. 由主接口通过 HTTP 调用。两模型各自常驻内存,单图实时延迟最低。
  10. 运行环境:paddleocr conda 环境(paddleocr 3.2.0 + paddlepaddle-gpu 3.2.0)
  11. 启动:
  12. CUDA_VISIBLE_DEVICES=<空闲V100 UUID> \
  13. ~/miniconda3/envs/paddleocr/bin/python serve_lang_judge.py --port 8100
  14. 接口:
  15. POST /lang_judge multipart 上传字段 image=<YOLO裁剪图>
  16. → {"language": "tcg us"|"tcg jp"|"简中"|"繁中"|null,
  17. "angle": 0|90|180|270,
  18. "detail": {...字符统计...},
  19. "text": "OCR文本(≤500字符)"}
  20. GET /health → {"status": "ok"}
  21. 依赖:pip install flask (paddleocr / hanzidentifier 环境已有)
  22. 说明:核心 ocr_full_text() 与 judge() 逻辑直接取自本框架 ocr_judge.py:40-95,
  23. 原样保留判定规则(假名≥10→jp;cjk*3>latin→中文再判简繁;否则→us),
  24. 仅去掉其面向 CSV 批处理的主循环,改为按请求单张处理。不改动 ocr_judge.py。
  25. """
  26. import os
  27. import sys
  28. import argparse
  29. import tempfile
  30. import traceback
  31. # 强制 UTF-8 输出(Windows 控制台默认 GBK,打印中文会报错)
  32. sys.stdout.reconfigure(encoding="utf-8")
  33. MAX_TEXT_LEN = 500 # OCR 文本最大字符数(与 ocr_judge.py 一致)
  34. # ==================== 启动时一次性加载 PaddleOCR(常驻内存)====================
  35. print("[sidecar] 初始化 PaddleOCR 3.2.0 ...", flush=True)
  36. from paddleocr import PaddleOCR
  37. # use_doc_orientation_classify=True 必要:关闭时旋转卡 OCR 会乱码;开启后顺带给出方向角
  38. ocr = PaddleOCR(
  39. use_doc_orientation_classify=True,
  40. use_doc_unwarping=False,
  41. use_textline_orientation=True,
  42. )
  43. print("[sidecar] PaddleOCR 加载完成", flush=True)
  44. import hanzidentifier as hz
  45. # ==================== OCR 文本 + 方向角(取自 ocr_judge.py:40-69)====================
  46. def ocr_full_text(img_path):
  47. """OCR 识别,返回 (完整文本, 方向角度)。
  48. 角度来自 doc_preprocessor_res.angle(0/90/180/270,输入相对正立顺时针的旋转)。"""
  49. result = ocr.predict(img_path)
  50. texts, angle = [], 0
  51. for res in result:
  52. if isinstance(res, dict):
  53. t = res.get("rec_texts")
  54. if t:
  55. texts.extend(list(t))
  56. dpr = res.get("doc_preprocessor_res")
  57. if dpr is not None:
  58. a = dpr.get("angle") if isinstance(dpr, dict) else getattr(dpr, "angle", None)
  59. if a is not None:
  60. try:
  61. angle = int(a)
  62. except Exception:
  63. angle = a
  64. elif hasattr(res, "rec_texts") and res.rec_texts:
  65. texts.extend(list(res.rec_texts))
  66. dpr = getattr(res, "doc_preprocessor_res", None)
  67. if dpr is not None:
  68. a = getattr(dpr, "angle", None)
  69. if a is not None:
  70. try:
  71. angle = int(a)
  72. except Exception:
  73. angle = a
  74. return "".join(texts), angle
  75. # ==================== 语种判定(取自 ocr_judge.py:76-95)====================
  76. def judge(text):
  77. """语种判定逻辑:
  78. ① 假名>=10 → tcg jp (日文铁证)
  79. ② 中文占比>25% (cjk*3 > latin) → 是中文卡 → hanzidentifier 判简繁
  80. ③ 否则 (cjk*3 <= latin) → tcg us (英文)
  81. 卡牌上英文元素(HP/Weakness/版权/编号)天然拉高latin,故中文阈值放宽到25%。"""
  82. kana = sum(1 for ch in text if 0x3040 <= ord(ch) < 0x3100)
  83. cjk = sum(1 for ch in text if 0x4E00 <= ord(ch) < 0x9FFF)
  84. latin = sum(1 for ch in text if ch.isascii() and ch.isalpha())
  85. if kana >= 10:
  86. return "tcg jp", {"kana": kana, "cjk": cjk, "latin": latin}
  87. if cjk * 3 > latin:
  88. s = sum(1 for ch in text if hz.identify(ch) == 2) # 简体字
  89. t = sum(1 for ch in text if hz.identify(ch) == 1) # 繁体字
  90. script = "简中" if s >= t else "繁中"
  91. return script, {"kana": kana, "s": s, "t": t, "cjk": cjk}
  92. return "tcg us", {"kana": kana, "latin": latin, "cjk": cjk}
  93. def judge_image(img_path):
  94. """单张裁剪图 → {language, angle, detail, text}。异常时 language=None。"""
  95. try:
  96. full_text, angle = ocr_full_text(img_path)
  97. text = full_text[:MAX_TEXT_LEN]
  98. lang, det = judge(text)
  99. return {"language": lang, "angle": angle, "detail": det, "text": text}
  100. except Exception as e:
  101. traceback.print_exc()
  102. return {"language": None, "angle": 0, "detail": {"err": str(e)[:120]}, "text": ""}
  103. # ==================== HTTP 服务(Flask)====================
  104. def create_app():
  105. from flask import Flask, request, jsonify
  106. app = Flask(__name__)
  107. @app.get("/health")
  108. def health():
  109. return jsonify({"status": "ok"})
  110. @app.post("/lang_judge")
  111. def lang_judge():
  112. """接收 multipart 图片(字段名 image),OCR + 判语种/方向,返回 JSON。"""
  113. up = request.files.get("image")
  114. if not up or not up.filename:
  115. return jsonify({"error": "缺少图片:请以 multipart 字段 image 上传"}), 400
  116. blob = up.read()
  117. if not blob:
  118. return jsonify({"error": "上传文件为空"}), 400
  119. # 存临时文件供 PaddleOCR predict(推理后删)
  120. fd, tmp = tempfile.mkstemp(suffix=".jpg")
  121. try:
  122. with os.fdopen(fd, "wb") as f:
  123. f.write(blob)
  124. return jsonify(judge_image(tmp))
  125. finally:
  126. if os.path.exists(tmp):
  127. try:
  128. os.remove(tmp)
  129. except Exception:
  130. pass
  131. return app
  132. if __name__ == "__main__":
  133. p = argparse.ArgumentParser(description="语种/方向判断 sidecar (PaddleOCR)")
  134. p.add_argument("--host", default="127.0.0.1", help="监听地址,默认 127.0.0.1(仅本机主接口调用)")
  135. p.add_argument("--port", type=int, default=8100, help="监听端口,默认 8100")
  136. args = p.parse_args()
  137. print("[sidecar] 就绪,监听 %s:%d,等待请求" % (args.host, args.port), flush=True)
  138. create_app().run(host=args.host, port=args.port, threaded=True)