bubble-detector / ocr.py
VaiiZ's picture
Upload 3 files
387636a verified
Raw History Blame Contribute Delete
3.8 kB
import io
import os
import re
import numpy as np
from PIL import Image
# Facteur d'agrandissement appliqué aux crops de bulles AVANT l'OCR. Les
# bulles croppées sont souvent petites ; agrandir l'image (LANCZOS) améliore
# nettement la reconnaissance de PaddleOCR (et de manga-ocr) sur les polices
# manga stylisées et le texte de petite taille. Surchargeable via
# OCR_UPSCALE (1 = désactivé).
UPSCALE_FACTOR = int(os.environ.get("OCR_UPSCALE", "2"))
def _prepare_image(image_bytes: bytes) -> Image.Image:
"""Charge l'image et l'agrandit (LANCZOS) pour améliorer la précision OCR."""
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
if UPSCALE_FACTOR > 1:
w, h = image.size
image = image.resize(
(w * UPSCALE_FACTOR, h * UPSCALE_FACTOR), Image.LANCZOS
)
return image
def _clean_ocr_text(text: str) -> str:
"""Nettoyage LÉGER du texte OCR.
Ne corrige PAS les mots (risque de casser des noms propres ou des mots
composés), mais retire le bruit évident : espaces multiples, espaces
avant ponctuation, espaces en début/fin.
"""
text = re.sub(r"\s+", " ", text)
text = re.sub(r"\s+([,.;:!?…])", r"\1", text)
return text.strip()
# Mapping des codes ISO 639-1 (apps/web/lib/languages.ts) vers les codes de
# langue de PaddleOCR 2.x. Les langues à alphabet latin sans modèle dédié
# (espagnol, italien, portugais, néerlandais...) utilisent le modèle
# générique `latin`. Le japonais (ja) utilise le modèle dédié `japan`.
PADDLE_LANG_MAP = {
"ja": "japan",
"de": "german",
"en": "en",
"es": "latin",
"it": "latin",
"pt": "latin",
"nl": "latin",
"fr": "french",
"ru": "cyrillic",
"ko": "korean",
"zh": "ch",
}
class PaddleOcr:
"""
OCR (PaddleOCR) pour TOUTES les langues, dont le japonais (lang='japan'),
exécuté côté serveur. Bien plus robuste que Tesseract.js sur les polices
manga stylisées, et déporté côté serveur — aligné avec l'objectif de mise
à disposition publique (l'utilisateur final n'a rien à installer).
NB : manga-ocr (plus précis sur le japonais) a été retiré car il dépend
de transformers, incompatible avec gradio 6.24.0 imposé par HF Spaces
ZeroGPU (conflit huggingface-hub). Le japonais passe donc par PaddleOCR.
Les modèles PaddleOCR se téléchargent automatiquement depuis les serveurs
de PaddleOCR au premier appel, puis restent en cache dans le conteneur.
"""
def __init__(self) -> None:
self._reader = None
self._lang = None
def _ensure_loaded(self, lang: str):
# Un lecteur PaddleOCR est lié à une langue : on en recrée un si la
# langue demandée change (coût unique, mis en cache ensuite).
if self._reader is None or self._lang != lang:
from paddleocr import PaddleOCR
self._reader = PaddleOCR(
use_angle_cls=True,
lang=lang,
show_log=False,
)
self._lang = lang
return self._reader
def read(self, image_bytes: bytes, lang: str) -> str:
reader = self._ensure_loaded(lang)
image = _prepare_image(image_bytes)
result = reader.ocr(np.array(image), cls=True)
# PaddleOCR 2.x renvoie une liste de listes : pour une seule image,
# result[0] est la liste des détections, chacune au format
# [box, (texte, confiance)]. On concatène les textes détectés.
lines: list[str] = []
if result and result[0]:
for detection in result[0]:
text = detection[1][0]
if text and text.strip():
lines.append(_clean_ocr_text(text))
return "\n".join(lines)