Files
rafa 66ee943da4 chore(verify): recuperar enum_urls.php y commitear tooling de verificación #121
El enumerador de URLs no se había commiteado y se perdió. Recuperado
reproduciendo la composición de la corrida previa (menús×5 idiomas +
categorías con contenido + contenido no-ES). Se añade también
classify_gaps.py (clasifica huecos: traducir vs descargar) y run_all.sh.
Outputs y .venv quedan en .gitignore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:30:18 -04:00

49 lines
2.6 KiB
Python

#!/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")