feat: process mining BPMN, détection changement écran pHash, OCR docTR
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 12s
security-audit / pip-audit (CVE dépendances) (push) Successful in 10s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 15s
tests / Tests unitaires (sans GPU) (push) Failing after 13s
tests / Tests sécurité (critique) (push) Has been skipped
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 12s
security-audit / pip-audit (CVE dépendances) (push) Successful in 10s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 15s
tests / Tests unitaires (sans GPU) (push) Failing after 13s
tests / Tests sécurité (critique) (push) Has been skipped
Process Mining (core/analytics/process_mining_bridge.py) : - Bridge PM4Py : conversion sessions Shadow → event log → BPMN XML + PNG - KPIs automatiques : durée, variantes, goulots, distribution par app - Support sessions JSONL brutes et workflows core JSON - 42 tests (dont 1 sur données réelles) Détection changement d'écran (core/analytics/screen_change_detector.py) : - pHash (imagehash) : ~16ms par screenshot, seuils SAME/MINOR/MAJOR - 8 tests sur screenshots réels OCR docTR dans execute_extract_text : - docTR par défaut pour lecture simple (rapide, CPU) - Ollama VLM en fallback ou sur demande explicite (mode "vlm"/"ai") - Dual-mode adaptatif selon extraction_mode Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -534,8 +534,11 @@ def execute_ai_analyze(params: dict) -> dict:
|
||||
|
||||
def execute_extract_text(params: dict) -> dict:
|
||||
"""
|
||||
Extrait du texte depuis l'écran via Ollama VLM.
|
||||
Capture la zone de l'ancre (ou l'écran entier) et demande au VLM d'extraire le texte.
|
||||
Extrait du texte depuis l'écran.
|
||||
|
||||
Stratégie : docTR (OCR local rapide) par défaut pour les modes simples
|
||||
(full, lines, words, numbers). Fallback sur Ollama VLM pour le mode
|
||||
"vlm"/"ai" ou si docTR échoue.
|
||||
"""
|
||||
import requests
|
||||
import re
|
||||
@@ -549,7 +552,9 @@ def execute_extract_text(params: dict) -> dict:
|
||||
extraction_mode = params.get('extraction_mode', 'full')
|
||||
text_filters = params.get('text_filters', [])
|
||||
|
||||
# --- 1. Capture de l'image ---
|
||||
screenshot_base64 = anchor.get('screenshot') if anchor else None
|
||||
pil_image = None # On garde l'image PIL pour docTR
|
||||
|
||||
if not screenshot_base64:
|
||||
try:
|
||||
@@ -562,56 +567,100 @@ def execute_extract_text(params: dict) -> dict:
|
||||
x, y = int(bbox.get('x', 0)), int(bbox.get('y', 0))
|
||||
w, h = int(bbox.get('width', 100)), int(bbox.get('height', 100))
|
||||
print(f"📸 [OCR] Capture zone: ({x}, {y}) -> ({x+w}, {y+h})")
|
||||
screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h))
|
||||
pil_image = ImageGrab.grab(bbox=(x, y, x + w, y + h))
|
||||
else:
|
||||
print(f"📸 [OCR] Capture écran complet")
|
||||
screenshot = ImageGrab.grab()
|
||||
pil_image = ImageGrab.grab()
|
||||
|
||||
buffer = io.BytesIO()
|
||||
screenshot.save(buffer, format='PNG')
|
||||
pil_image.save(buffer, format='PNG')
|
||||
screenshot_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
except Exception as cap_err:
|
||||
return {'success': False, 'error': f"Erreur capture: {cap_err}"}
|
||||
else:
|
||||
# Décoder le base64 en image PIL pour docTR
|
||||
try:
|
||||
import io
|
||||
from PIL import Image as PILImage
|
||||
img_bytes = base64.b64decode(screenshot_base64)
|
||||
pil_image = PILImage.open(io.BytesIO(img_bytes))
|
||||
except Exception:
|
||||
pil_image = None
|
||||
|
||||
if not screenshot_base64:
|
||||
return {'success': False, 'error': "Pas d'image à analyser"}
|
||||
|
||||
prompt_map = {
|
||||
'full': "Extrais TOUT le texte visible dans cette image. Retourne uniquement le texte brut, sans commentaire.",
|
||||
'numbers': "Extrais uniquement les nombres et chiffres visibles dans cette image. Retourne-les séparés par des espaces.",
|
||||
'lines': "Extrais tout le texte visible ligne par ligne. Une ligne par ligne de texte visible.",
|
||||
'words': "Extrais tous les mots visibles dans cette image, séparés par des espaces.",
|
||||
}
|
||||
prompt = prompt_map.get(extraction_mode, prompt_map['full'])
|
||||
# --- 2. Modes docTR (rapide) vs VLM (raisonnement) ---
|
||||
use_vlm = extraction_mode in ('vlm', 'ai')
|
||||
extracted_text = None
|
||||
engine_used = None
|
||||
|
||||
if 'qwen' in model.lower() and not prompt.startswith('/no_think'):
|
||||
prompt = f"/no_think\n{prompt}"
|
||||
if not use_vlm and pil_image is not None:
|
||||
# Essayer docTR d'abord pour les modes simples
|
||||
try:
|
||||
from visual_workflow_builder.backend.services.ocr_service import (
|
||||
ocr_extract_text,
|
||||
)
|
||||
print(f"📝 [OCR] Extraction texte via docTR (mode: {extraction_mode})...")
|
||||
raw_text = ocr_extract_text(pil_image)
|
||||
|
||||
print(f"📝 [OCR] Extraction texte avec {model} (mode: {extraction_mode})...")
|
||||
if raw_text and raw_text.strip():
|
||||
extracted_text = raw_text.strip()
|
||||
engine_used = "doctr"
|
||||
print(f"✅ [OCR] docTR OK ({len(extracted_text)} caractères)")
|
||||
else:
|
||||
print("⚠️ [OCR] docTR n'a rien extrait, fallback Ollama VLM")
|
||||
except ImportError:
|
||||
print("⚠️ [OCR] docTR non disponible, fallback Ollama VLM")
|
||||
except Exception as doctr_err:
|
||||
print(f"⚠️ [OCR] Erreur docTR: {doctr_err}, fallback Ollama VLM")
|
||||
|
||||
ollama_url = params.get('ollama_url', 'http://localhost:11434')
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"images": [screenshot_base64],
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.1, "num_predict": 4000}
|
||||
}
|
||||
# --- 3. Fallback Ollama VLM si docTR n'a rien donné ---
|
||||
if extracted_text is None:
|
||||
prompt_map = {
|
||||
'full': "Extrais TOUT le texte visible dans cette image. Retourne uniquement le texte brut, sans commentaire.",
|
||||
'numbers': "Extrais uniquement les nombres et chiffres visibles dans cette image. Retourne-les séparés par des espaces.",
|
||||
'lines': "Extrais tout le texte visible ligne par ligne. Une ligne par ligne de texte visible.",
|
||||
'words': "Extrais tous les mots visibles dans cette image, séparés par des espaces.",
|
||||
'vlm': "Extrais TOUT le texte visible dans cette image. Retourne uniquement le texte brut, sans commentaire.",
|
||||
'ai': "Extrais TOUT le texte visible dans cette image. Retourne uniquement le texte brut, sans commentaire.",
|
||||
}
|
||||
prompt = prompt_map.get(extraction_mode, prompt_map['full'])
|
||||
|
||||
response = requests.post(
|
||||
f"{ollama_url}/api/generate",
|
||||
json=payload,
|
||||
timeout=timeout_ms / 1000
|
||||
)
|
||||
if 'qwen' in model.lower() and not prompt.startswith('/no_think'):
|
||||
prompt = f"/no_think\n{prompt}"
|
||||
|
||||
if response.status_code != 200:
|
||||
return {'success': False, 'error': f"Erreur Ollama: {response.status_code}"}
|
||||
print(f"📝 [OCR] Extraction texte avec {model} (mode: {extraction_mode})...")
|
||||
|
||||
result = response.json()
|
||||
extracted_text = result.get('response', '').strip()
|
||||
ollama_url = params.get('ollama_url', 'http://localhost:11434')
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"images": [screenshot_base64],
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.1, "num_predict": 4000}
|
||||
}
|
||||
|
||||
if not extracted_text and result.get('thinking'):
|
||||
extracted_text = result.get('thinking', '').strip()
|
||||
response = requests.post(
|
||||
f"{ollama_url}/api/generate",
|
||||
json=payload,
|
||||
timeout=timeout_ms / 1000
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {'success': False, 'error': f"Erreur Ollama: {response.status_code}"}
|
||||
|
||||
result = response.json()
|
||||
extracted_text = result.get('response', '').strip()
|
||||
|
||||
if not extracted_text and result.get('thinking'):
|
||||
extracted_text = result.get('thinking', '').strip()
|
||||
|
||||
engine_used = f"ollama/{model}"
|
||||
|
||||
# --- 4. Application des filtres ---
|
||||
if extracted_text is None:
|
||||
extracted_text = ""
|
||||
|
||||
for f in text_filters:
|
||||
if f == 'digits_only':
|
||||
@@ -625,7 +674,7 @@ def execute_extract_text(params: dict) -> dict:
|
||||
elif f == 'lowercase':
|
||||
extracted_text = extracted_text.lower()
|
||||
|
||||
print(f"✅ [OCR] Texte extrait ({len(extracted_text)} caractères)")
|
||||
print(f"✅ [OCR] Texte extrait ({len(extracted_text)} caractères) via {engine_used}")
|
||||
if extracted_text:
|
||||
print(f" Résultat: {extracted_text[:150]}...")
|
||||
|
||||
@@ -639,7 +688,7 @@ def execute_extract_text(params: dict) -> dict:
|
||||
'character_count': len(extracted_text),
|
||||
'word_count': len(extracted_text.split()) if extracted_text else 0,
|
||||
'mode': extraction_mode,
|
||||
'model': model
|
||||
'engine': engine_used
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user