Terminal / api /gemini_vision.py
Pulka
fix(runtime): add missing api/gemini_vision.py — resolves RUNTIME_ERROR ModuleNotFoundError
ca1d338 verified
Raw
History Blame Contribute Delete
8.9 kB
"""
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,
}