#!/usr/bin/env python3 # Capa 3 (opcional): review semántica con Gemma LOCAL, SOLO sobre páginas marcadas por la Capa 1. # Coste 0 (LLM local). Si Gemma no responde, sale limpio y deja constancia. import json, os, urllib.request DIR=os.path.dirname(os.path.abspath(__file__)) GEMMA="http://172.19.128.1:1234/v1/chat/completions"; MODEL="google/gemma-4-e4b" MAX=int(os.environ.get('MAX','40')) LNAME={'en':'inglés','fr':'francés','it':'italiano','pt':'portugués','es':'español'} # texto visible está en report.jsonl (no en report.json) texts={} for line in open(os.path.join(DIR,'report.jsonl'),encoding='utf-8'): line=line.strip() if not line: continue r=json.loads(line); texts[(r['lang'],r['url'])]=r.get('text','') rep=json.load(open(os.path.join(DIR,'report.json'),encoding='utf-8')) flagged=[r for r in rep if any(f in r.get('flags',[]) for f in ('SPANISH_LEAK','LANG_MISMATCH'))][:MAX] print(f"páginas marcadas para review LLM: {len(flagged)} (cap {MAX})") def ask(lang,text): prompt=(f"Eres revisor de QA de un sitio web. Esta página DEBERÍA estar en {LNAME.get(lang,lang)}. " f"Evalúa el texto visible. Responde SOLO un JSON válido: " f'{{"idioma_ok": true/false, "idioma_predominante": "es/en/fr/it/pt", "problemas": ["lista breve de mistraducciones, trozos sin traducir o sin sentido"]}}.\n\nTEXTO:\n'+text[:1500]) body=json.dumps({"model":MODEL,"messages":[{"role":"user","content":prompt}],"temperature":0.1,"max_tokens":2200}).encode() req=urllib.request.Request(GEMMA,data=body,headers={"Content-Type":"application/json"}) r=json.load(urllib.request.urlopen(req,timeout=120)) out=r["choices"][0]["message"]["content"].strip() s=out.find('{'); e=out.rfind('}') return json.loads(out[s:e+1]) if s>=0 else {"raw":out[:200]} results=[] try: urllib.request.urlopen("http://172.19.128.1:1234/v1/models",timeout=6) except Exception as e: print("Gemma no responde, salto Capa 3:",e); json.dump({"skipped":True,"reason":str(e)},open(os.path.join(DIR,'llm_review.json'),'w')); raise SystemExit(0) for i,r in enumerate(flagged): t=texts.get((r['lang'],r['url']),'') if len(t)<60: continue try: v=ask(r['lang'],t); v.update({'url':r['url'],'lang':r['lang'],'flags':r['flags']}); results.append(v) print(f" [{i+1}/{len(flagged)}] {r['lang']} ok={v.get('idioma_ok')} pred={v.get('idioma_predominante')}") except Exception as e: print(f" [{i+1}] error {e}") json.dump(results,open(os.path.join(DIR,'llm_review.json'),'w',encoding='utf-8'),ensure_ascii=False,indent=1) print(f"review LLM guardada: {len(results)} páginas")