backup: snapshot post-démo GHT 2026-05-19
Some checks failed
tests / Lint (ruff + black) (push) Successful in 1m50s
tests / Tests unitaires (sans GPU) (push) Failing after 1m50s
tests / Tests sécurité (critique) (push) Has been skipped

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:
Dom
2026-05-19 14:55:06 +02:00
parent f2212e77e3
commit 5ea4960e65
627 changed files with 211348 additions and 169 deletions

View File

@@ -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 []