Spaces:
Configuration error
Configuration error
Pulka
fix(runtime): add missing api/gemini_vision.py — resolves RUNTIME_ERROR ModuleNotFoundError
ca1d338 verified | """ | |
| 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 ──────────────────────────────────────────────────────────────── | |
| 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) | |
| 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, | |
| } | |