backup: snapshot post-démo GHT 2026-05-19
Backup état complet après enregistrement vidéo démo de bout en bout. À utiliser comme point de référence pour la consolidation post-démo. Changements majeurs de la session 18-19 mai : - AIVA-URGENCE : page autonome avec preset URL + auto-focus chain - Workflow Demo_urgence_3_db : merge linux_db + steps AIVA + pause humaine NoMachine - Bypass LLM (static_result / static_text) dans replay_engine pour démos déterministes sans appel Ollama - Fix api_stream:3013 — replay_paused au premier polling /next - dag_execute : lift duration_ms vers top-level pour wait runtime - NPM bypass auth /aiva-urgence/ via location ^~ (proxy_host/10.conf hors git) - scripts/cancel-replays.sh — workaround Stop VWB qui ne purge pas la queue Anchors visuels (468) forcés dans le commit pour garantir restorabilité. DB workflows actuelle + ~12 .bak DB de la journée incluses. Sujets identifiés pour consolidation post-démo (TODO) : 1. Bug VWB recapture anchor ne régénère pas le PNG 2. Léa client accumule état mémoire (restart périodique requis) 3. Stop VWB ne purge pas la queue serveur (lien manquant vers /replay/cancel) 4. Bug coord client mss tronqué 2560x60 → mapping Y cassé 5. delay_before/delay_after ignorés au runtime (fix partiel duration_ms) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,16 +2,18 @@
|
||||
|
||||
Utilise EasyOCR fr+en. Singleton (chargement modèle ~3s au premier appel).
|
||||
|
||||
Conçu pour le pipeline streaming serveur (action `extract_text`) : récupère
|
||||
un screenshot fresh (dernier heartbeat ou capture forcée), applique l'OCR,
|
||||
retourne le texte concaténé pour analyse downstream (ex: t2a_decision).
|
||||
Conçu pour le pipeline streaming serveur (actions `extract_text` /
|
||||
`extract_table`) : récupère un screenshot fresh (dernier heartbeat ou
|
||||
capture forcée), applique l'OCR, retourne le texte ou une liste structurée
|
||||
pour analyse downstream (ex: t2a_decision, boucle sur N patients).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,16 +21,16 @@ _easyocr_reader = None
|
||||
|
||||
|
||||
def _get_reader():
|
||||
"""Initialise EasyOCR fr+en au premier appel (singleton)."""
|
||||
"""Initialise EasyOCR fr+en au premier appel (singleton, CPU forcé).
|
||||
|
||||
CPU forcé : le streaming server partage la VRAM avec Ollama (qwen2.5:7b ~5GB)
|
||||
et les modèles CLIP/FAISS — pas assez de marge pour EasyOCR GPU (~1GB).
|
||||
"""
|
||||
global _easyocr_reader
|
||||
if _easyocr_reader is None:
|
||||
import easyocr
|
||||
try:
|
||||
_easyocr_reader = easyocr.Reader(['fr', 'en'], gpu=True, verbose=False)
|
||||
logger.info("EasyOCR initialisé (fr+en, GPU)")
|
||||
except Exception as e:
|
||||
logger.warning("EasyOCR GPU indisponible (%s), fallback CPU", e)
|
||||
_easyocr_reader = easyocr.Reader(['fr', 'en'], gpu=False, verbose=False)
|
||||
_easyocr_reader = easyocr.Reader(['fr', 'en'], gpu=False, verbose=False)
|
||||
logger.info("EasyOCR initialisé (fr+en, CPU)")
|
||||
return _easyocr_reader
|
||||
|
||||
|
||||
@@ -69,3 +71,70 @@ def extract_text_from_image(
|
||||
except Exception as e:
|
||||
logger.warning("extract_text échoué sur %s : %s", image_path, e)
|
||||
return ""
|
||||
|
||||
|
||||
def extract_table_from_image(
|
||||
image_path: str,
|
||||
region: Optional[Tuple[int, int, int, int]] = None,
|
||||
pattern: Optional[str] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> List[str]:
|
||||
"""Extrait une liste de valeurs d'un tableau via OCR.
|
||||
|
||||
Cas d'usage principal : lire la liste des IPP d'un tableau de patients
|
||||
pour boucler dessus. EasyOCR retourne tous les tokens avec leur bbox,
|
||||
on filtre par regex puis on trie par position (y croissant).
|
||||
|
||||
Args:
|
||||
image_path: chemin du PNG sur disque.
|
||||
region: (x, y, w, h) pour cropper avant OCR. None = image entière.
|
||||
pattern: regex Python ; seuls les tokens qui matchent sont conservés.
|
||||
Si None : tous les tokens non vides sont retournés.
|
||||
Exemple IPP : r"^\\d{8,10}$" ou r"^25\\d{6}$"
|
||||
limit: nombre maximal d'entrées à retourner (None = sans limite).
|
||||
|
||||
Returns:
|
||||
Liste de strings dans l'ordre top → bottom (par y de bbox).
|
||||
En cas d'erreur, retourne une liste vide et log un warning.
|
||||
"""
|
||||
path = Path(image_path)
|
||||
if not path.exists():
|
||||
logger.warning("extract_table: fichier introuvable %s", image_path)
|
||||
return []
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
img = Image.open(path)
|
||||
if region:
|
||||
x, y, w, h = region
|
||||
img = img.crop((x, y, x + w, y + h))
|
||||
|
||||
reader = _get_reader()
|
||||
# detail=1 ⇒ chaque résultat = (bbox, text, confidence)
|
||||
# bbox est une liste de 4 points [tl, tr, br, bl]
|
||||
results = reader.readtext(np.array(img), detail=1, paragraph=False)
|
||||
|
||||
compiled = re.compile(pattern) if pattern else None
|
||||
|
||||
rows: List[Tuple[float, str]] = []
|
||||
for bbox, text, _conf in results:
|
||||
t = str(text).strip()
|
||||
if not t:
|
||||
continue
|
||||
if compiled and not compiled.match(t):
|
||||
continue
|
||||
# y moyen pour tri vertical (top→bottom)
|
||||
ys = [p[1] for p in bbox]
|
||||
y_mean = sum(ys) / len(ys)
|
||||
rows.append((y_mean, t))
|
||||
|
||||
rows.sort(key=lambda r: r[0])
|
||||
values = [t for _y, t in rows]
|
||||
if limit:
|
||||
values = values[:limit]
|
||||
return values
|
||||
except Exception as e:
|
||||
logger.warning("extract_table échoué sur %s : %s", image_path, e)
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user