""" gemini_vision.py — P48: Gemini 1.5 Flash Vision (Manus Killer). Modulo dedicato per analisi immagini con Gemini 1.5 Flash: - Free tier: 15 RPM, 1M token/giorno (AI Studio key, zero costi) - POST /api/vision/gemini — analisi diretta Gemini (bypass fallback chain) - POST /api/vision/screenshot_analyze — Playwright screenshot + Gemini in un'unica call Gap chiuso (P48): - Prima: analyze_image richiedeva GROQ_API_KEY, Gemini era solo fallback #2 - Dopo: Gemini disponibile come provider primario zero-cost - Caso d'uso critico: agente genera app → scatta screenshot → Gemini trova glitch visivi → agente si auto-corregge senza supervisione umana @module gemini_vision """ import os, base64, httpx, logging from fastapi import APIRouter from pydantic import BaseModel router = APIRouter(prefix="/api/vision", tags=["gemini_vision"]) _logger = logging.getLogger("gemini_vision") _GEMINI_KEY = os.getenv("GEMINI_API_KEY", "") _GEMINI_BASE = "https://generativelanguage.googleapis.com/v1beta/models" _GEMINI_MODEL = "gemini-1.5-flash" _USER_AGENT = "Mozilla/5.0 (compatible; AgenteAI/3.0)" # ─── Modelli ────────────────────────────────────────────────────────────────── class GeminiAnalyzeRequest(BaseModel): url: str = "" base64_image: str = "" question: str = "Analizza questa immagine in dettaglio. Descrivi cosa vedi, identifica problemi visivi o errori UI." model: str = "gemini-1.5-flash" max_tokens: int = 800 class ScreenshotAnalyzeRequest(BaseModel): url: str question: str = "Analizza questo screenshot come UI tester esperto. Identifica: 1) Errori visivi (layout rotto, testo troncato, overlap). 2) Errori JS o console visibili. 3) Elementi mancanti o mal posizionati. 4) Problemi di responsive. Rispondi in italiano con bullet points." mobile: bool = False width: int = 1280 # ─── Core helper ────────────────────────────────────────────────────────────── async def gemini_analyze( image_b64: str, image_mime: str, question: str, model: str = "gemini-1.5-flash", max_tokens: int = 800, api_key: str = "", ) -> dict: """ Chiama Gemini Vision API con immagine in base64. Riutilizzata da vision.py (fallback chain) e dagli endpoint di questo modulo. Ritorna {"ok": True, "description": str, "provider": str} oppure {"ok": False, "error": str}. """ key = api_key or _GEMINI_KEY if not key: return {"ok": False, "error": "GEMINI_API_KEY non configurato."} if not image_b64: return {"ok": False, "error": "Nessuna immagine fornita."} payload = { "contents": [{ "parts": [ {"inline_data": {"mime_type": image_mime, "data": image_b64}}, {"text": question}, ] }], "generationConfig": {"maxOutputTokens": max_tokens, "temperature": 0.4}, } try: async with httpx.AsyncClient(timeout=40) as c: r = await c.post( f"{_GEMINI_BASE}/{model}:generateContent?key={key}", headers={"Content-Type": "application/json"}, json=payload, ) if r.status_code == 429: return {"ok": False, "error": "Gemini rate limit (15 RPM). Riprova tra qualche secondo."} if not r.is_success: return {"ok": False, "error": f"Gemini API {r.status_code}: {r.text[:300]}"} _cands = r.json().get("candidates") or [] _parts = (_cands[0].get("content", {}).get("parts") or []) if _cands else [] _desc = next((p.get("text", "") for p in _parts if "text" in p), "") if not _desc: return {"ok": False, "error": "Gemini: risposta vuota (candidates vuoti)."} return {"ok": True, "description": _desc, "provider": f"gemini-{model.split('-')[-1]}"} except Exception as e: _logger.debug("gemini_analyze: %s", type(e).__name__) return {"ok": False, "error": f"Errore Gemini: {str(e)[:200]}"} # ─── Endpoints ──────────────────────────────────────────────────────────────── @router.post("/gemini") async def gemini_vision_direct(req: GeminiAnalyzeRequest): """ POST /api/vision/gemini — Analisi diretta Gemini Vision (bypass fallback chain). Accetta url o base64_image. Provider primario zero-cost per vision tasks. """ image_b64 = req.base64_image image_mime = "image/jpeg" if not image_b64 and req.url: try: async with httpx.AsyncClient(timeout=15, follow_redirects=True) as c: r = await c.get(req.url, headers={"User-Agent": _USER_AGENT}) if r.status_code != 200: return {"ok": False, "error": f"Download fallito: HTTP {r.status_code}"} ct = r.headers.get("content-type", "") image_mime = "image/png" if "png" in ct else "image/webp" if "webp" in ct else "image/jpeg" image_b64 = base64.b64encode(r.content).decode() except Exception as e: return {"ok": False, "error": f"Errore download: {str(e)[:200]}"} if not image_b64: return {"ok": False, "error": "Fornisci url o base64_image."} return await gemini_analyze(image_b64, image_mime, req.question, req.model, req.max_tokens) @router.post("/screenshot_analyze") async def screenshot_analyze(req: ScreenshotAnalyzeRequest): """ POST /api/vision/screenshot_analyze — Playwright screenshot + Gemini analyze. Pipeline (P48 Manus Killer): 1. Playwright screenshot della pagina (via _take_screenshot da api.browser) 2. Gemini 1.5 Flash analizza UI: glitch, errori, layout, responsive 3. Struttura il feedback per self-correction loop dell'agente Fallback: se Playwright non disponibile → scarica URL come immagine (se immagine diretta). """ if not _GEMINI_KEY: return { "ok": False, "error": "GEMINI_API_KEY non configurato.", "hint": "Aggiungi GEMINI_API_KEY in Railway Variables. Free su aistudio.google.com", } image_b64 = "" image_mime = "image/png" page_title = req.url # Step 1: Playwright screenshot (import interno — fail-safe se non disponibile) try: from api.browser import _take_screenshot as _shot # type: ignore[import] _result = await _shot(req.url, mobile=req.mobile, width=req.width, wait_ms=1500) if _result.get("ok") and _result.get("screenshot_b64"): image_b64 = _result["screenshot_b64"] page_title = _result.get("title", req.url) except Exception as _e: _logger.debug("screenshot_analyze: browser import failed: %s", _e) # Fallback: URL è un'immagine diretta if not image_b64: try: async with httpx.AsyncClient(timeout=15, follow_redirects=True) as c: r = await c.get(req.url, headers={"User-Agent": _USER_AGENT}) if r.is_success: ct = r.headers.get("content-type", "") if "image" in ct: image_mime = "image/png" if "png" in ct else "image/jpeg" image_b64 = base64.b64encode(r.content).decode() except Exception: pass if not image_b64: return { "ok": False, "error": f"Screenshot non disponibile per {req.url}.", "hint": "Verifica che il backend Railway abbia Playwright installato.", } # Step 2: Gemini analyze _q = req.question or ( "Sei un UI tester esperto. Analizza questo screenshot e identifica:\n" "1. Errori visivi (layout rotto, overlap, testo troncato, colori sbagliati)\n" "2. Errori o messaggi di errore visibili nella pagina\n" "3. Elementi mancanti o mal posizionati\n" "4. Problemi di accessibilita evidenti\n" "Rispondi con bullet points. Sii specifico sugli elementi problematici." ) _analysis = await gemini_analyze(image_b64, image_mime, _q, _GEMINI_MODEL, 1000) if not _analysis.get("ok"): return _analysis return { "ok": True, "description": _analysis["description"], "provider": _analysis.get("provider", "gemini-1.5-flash"), "page_title": page_title, "url": req.url, "screenshot_available": True, }