Spaces:
Running
Running
File size: 9,102 Bytes
28a08e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | """
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, Depends
from .auth_guard import require_role, AuthRole
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-2.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-2.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-2.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, role: AuthRole = Depends(require_role(AuthRole.MACHINE))): # GAP-1-fix
"""
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, role: AuthRole = Depends(require_role(AuthRole.MACHINE))): # GAP-1-fix
"""
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-2.5-flash"),
"page_title": page_title,
"url": req.url,
"screenshot_available": True,
}
|