Spaces:
Sleeping
Sleeping
File size: 6,400 Bytes
ba290fd | 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 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | """App Gradio pour Hugging Face Spaces (option C).
Remplace le serveur FastAPI (main.py) sur HF Spaces, car le SDK Docker y est
payant (PRO) alors que le SDK Gradio est GRATUIT (CPU basic, 16 Go RAM).
Réutilise INTÉGRALEMENT la logique de inference.py et ocr.py : la détection
de bulles (RT-DETR) et l'OCR (manga-ocr / PaddleOCR) sont inchangés. Seule
la couche d'exposition change : les 4 endpoints FastAPI deviennent des
fonctions Gradio, exposées via l'API HTTP de Gradio (/gradio_api/call/...).
Le secret partagé (BUBBLE_DETECTOR_SECRET) est vérifié dans chaque fonction
via gr.Request (en-tête X-API-Key), comme le middleware de main.py.
Lancement local :
pip install -r requirements.txt
python app.py
# puis tester l'API : POST /gradio_api/call/detect avec un champ "image"
"""
import os
import threading
from concurrent.futures import ThreadPoolExecutor
import gradio as gr
from inference import BubbleDetector
from ocr import PaddleOcr, PADDLE_LANG_MAP
detector = BubbleDetector()
paddle_ocr = PaddleOcr()
SECRET = os.environ.get("BUBBLE_DETECTOR_SECRET", "")
# ── ZeroGPU (Hugging Face Spaces) ──────────────────────────────────────────
# Sur le hardware ZeroGPU (option C, GRATUIT), les fonctions doivent être
# déclarées « GPU » via le décorateur `spaces.GPU` pour tourner sur la puce
# T4 partagée ; sinon elles s'exécutent sur CPU (lent, et consomment quand
# même du quota). Tout est best-effort : si le paquet `spaces` n'est pas
# installé (dev local / Docker FastAPI), les fonctions restent inchangées.
try:
from spaces import GPU as _SPACES_GPU # type: ignore
def _gpu(fn):
return _SPACES_GPU(fn)
except Exception: # ImportError ou environnement non-HF
def _gpu(fn):
return fn
# Nombre de bulles OCR traitées en parallèle dans ocr_batch. HF Spaces
# gratuit n'a que 2 vCPU : 2 est le bon compromis (voir Dockerfile.hf).
try:
_available_cpus = len(os.sched_getaffinity(0)) # Linux uniquement
except AttributeError:
_available_cpus = os.cpu_count() or 1
OCR_BATCH_WORKERS = int(os.environ.get("OCR_BATCH_WORKERS", "2"))
_ocr_executor = ThreadPoolExecutor(
max_workers=max(1, min(OCR_BATCH_WORKERS, _available_cpus))
)
# PaddleOCR (paddlepaddle 2.6.2) n'est PAS thread-safe pour l'inférence
# concurrente : on sérialise ses accès avec un verrou (voir main.py).
_paddle_lock = threading.Lock()
def _check_auth(request: gr.Request) -> None:
"""Refuse la requête si le secret partagé est défini et absent/incorrect."""
if SECRET and request.headers.get("x-api-key") != SECRET:
raise gr.Error("Clé API invalide.")
def _read_file(path: str) -> bytes:
with open(path, "rb") as f:
return f.read()
def _run_ocr(image_bytes: bytes, lang: str) -> str:
"""Exécute l'OCR d'UNE bulle, avec le bon moteur selon la langue."""
try:
if lang not in PADDLE_LANG_MAP:
return ""
with _paddle_lock:
return paddle_ocr.read(image_bytes, PADDLE_LANG_MAP[lang])
except Exception:
return ""
# --- Fonctions exposées via l'API Gradio --------------------------------
def detect(image, request: gr.Request):
"""Détecte les bulles de texte sur une image de page de manga."""
_check_auth(request)
return detector.detect(_read_file(image))
def ocr_japanese(image, request: gr.Request):
"""OCR japonais (PaddleOCR lang='japan') pour une bulle croppée."""
_check_auth(request)
with _paddle_lock:
return paddle_ocr.read(_read_file(image), "japan")
def ocr_general(image, lang, request: gr.Request):
"""OCR généraliste (PaddleOCR) pour une bulle croppée."""
_check_auth(request)
if lang not in PADDLE_LANG_MAP:
raise gr.Error(f"Langue non supportée par l'OCR généraliste : {lang}")
return paddle_ocr.read(_read_file(image), PADDLE_LANG_MAP[lang])
def ocr_batch(images, lang, request: gr.Request):
"""OCR de TOUTES les bulles d'une page en UN seul appel.
`images` est une liste de chemins de fichiers (gr.File file_count="multiple").
Renvoie une liste de textes dans le MÊME ORDRE que les images reçues.
"""
_check_auth(request)
payloads = [_read_file(p) for p in images]
texts = list(_ocr_executor.map(lambda p: _run_ocr(p, lang), payloads))
return texts
detect = _gpu(detect)
ocr_japanese = _gpu(ocr_japanese)
ocr_general = _gpu(ocr_general)
ocr_batch = _gpu(ocr_batch)
# --- Interface Gradio (UI minimale + API) -------------------------------
with gr.Blocks(title="Manga OCR") as demo:
gr.Markdown(
"## Manga Reader — détection de bulles + OCR\n"
"Service interne appelé par l'app web. Utilisez l'API "
"`/gradio_api/call/<nom>` (voir le guide)."
)
# Détection de bulles
# Gradio >= 6.0 (HF Spaces ZeroGPU impose gradio==6.24.0) : gr.File renvoie
# un chemin de fichier (str) par défaut ; les paramètres type= et filepath=
# ont été supprimés. La fonction reçoit donc directement le chemin.
detect_in = gr.File(label="Page image")
detect_out = gr.JSON(label="Bulles")
detect_btn = gr.Button("Détecter les bulles")
detect_btn.click(
fn=detect, inputs=detect_in, outputs=detect_out, api_name="detect"
)
# OCR japonais
ja_in = gr.File(label="Bulle (japonais)")
ja_out = gr.Textbox(label="Texte")
ja_btn = gr.Button("OCR japonais")
ja_btn.click(
fn=ocr_japanese, inputs=ja_in, outputs=ja_out, api_name="ocr_japanese"
)
# OCR généraliste
gen_img = gr.File(label="Bulle (généraliste)")
gen_lang = gr.Textbox(value="en", label="lang (ISO 639-1)")
gen_out = gr.Textbox(label="Texte")
gen_btn = gr.Button("OCR généraliste")
gen_btn.click(
fn=ocr_general,
inputs=[gen_img, gen_lang],
outputs=gen_out,
api_name="ocr_general",
)
# OCR par lot
batch_imgs = gr.File(file_count="multiple", label="Bulles (plusieurs)")
batch_lang = gr.Textbox(value="ja", label="lang (ISO 639-1)")
batch_out = gr.JSON(label="Textes")
batch_btn = gr.Button("OCR par lot")
batch_btn.click(
fn=ocr_batch,
inputs=[batch_imgs, batch_lang],
outputs=batch_out,
api_name="ocr_batch",
)
demo.queue()
demo.launch()
|