Spaces:
Running on Zero
Running on Zero
Download app.py from VaiiZ/bubble-detector: direct link, hf CLI and curl.
- Browser
- Download file 6.4 kB
-
https://huggingface.co/spaces/VaiiZ/bubble-detector/resolve/main/app.py
- Command line
-
hf download hf://spaces/VaiiZ/bubble-detector/app.py
-
curl -L -o app.py https://huggingface.co/spaces/VaiiZ/bubble-detector/resolve/main/app.py
6.4 kB
| """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() | |