Spaces:
Running on Zero
Running on Zero
File size: 3,795 Bytes
387636a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 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)
|