"""Tramoya backend — FastAPI sirve los estáticos y expone /api/summarize. El secret HF_TOKEN se inyecta como variable de entorno en HF Spaces (Settings → Variables and secrets). El navegador nunca lo ve. Usamos el router OpenAI-compatible de Hugging Face Inference Providers (https://router.huggingface.co/v1), que es la forma que HF documenta hoy. El viejo endpoint api-inference.huggingface.co devuelve 404 para la mayoría de modelos modernos (chat completions). """ from __future__ import annotations import logging import os from fastapi import FastAPI, HTTPException from fastapi.staticfiles import StaticFiles from openai import OpenAI from pydantic import BaseModel logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger("tramoya") HF_TOKEN = os.environ.get("HF_TOKEN") HF_MODEL = os.environ.get("HF_MODEL", "zai-org/GLM-5.2:together") HF_BASE_URL = os.environ.get("HF_BASE_URL", "https://router.huggingface.co/v1") STATIC_DIR = os.environ.get("STATIC_DIR", ".") SYSTEM_PROMPT = ( "Eres un asistente que resume escenas de guion cinematográfico. " "REGLAS DURAS, NO LAS ROMPAS:\n" "1. Solo describes lo que ocurre LITERALMENTE en el texto. " "Si una acción, objeto, sentimiento o nombre no aparece textualmente, " "no lo mencionas. Prohibido inventar.\n" "2. Si dudas si algo está en el texto, omítelo. Mejor un resumen " "incompleto que uno con datos inventados.\n" "3. Si el texto incluye cartelas, rótulos, créditos finales, títulos " "en pantalla o epílogos (líneas con prefijos como «Créditos:», " "«Cartel:», «Texto en pantalla:», «Rótulo:», «Sobreimpresión:» o " "similares), INCORPÓRALOS al resumen porque suelen ser el desenlace " "narrativo real de la escena. No los descartes como metadatos.\n" "4. Devuelves una frase, o dos como máximo. Hasta 40 palabras. " "Si la escena tiene cartelas o créditos al final, usa el extra de " "palabras para incluirlos. Si la escena es muy corta, el resumen " "también lo será.\n" "5. NO empiezas con frases como «en esta escena», «aquí», " "«la escena muestra», «vemos a». Empiezas directamente con la acción " "o el sujeto.\n" "6. Escribes en el mismo idioma del texto.\n" "7. No añades comentarios meta, encabezados, comillas ni prefijos." ) app = FastAPI(title="Tramoya") client = ( OpenAI(base_url=HF_BASE_URL, api_key=HF_TOKEN) if HF_TOKEN else None ) class SummarizeRequest(BaseModel): text: str heading: str = "" class SummarizeResponse(BaseModel): summary: str @app.get("/api/health") async def health() -> dict: return { "ok": True, "model": HF_MODEL, "base_url": HF_BASE_URL, "token_configured": bool(HF_TOKEN), } @app.post("/api/summarize", response_model=SummarizeResponse) async def summarize(req: SummarizeRequest) -> SummarizeResponse: if client is None: raise HTTPException( status_code=500, detail="HF_TOKEN no configurado en Settings del Space.", ) text = (req.text or "").strip() if len(text) < 40: return SummarizeResponse(summary=text[:200]) heading = (req.heading or "").strip() user_prompt = ( f"Encabezado de escena: {heading}\n\nContenido:\n{text[:4000]}" if heading else f"Contenido:\n{text[:4000]}" ) try: response = client.chat.completions.create( model=HF_MODEL, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}, ], max_tokens=140, temperature=0.1, ) raw = (response.choices[0].message.content or "") summary = raw.strip().strip('"“”\'').strip() return SummarizeResponse(summary=summary) except Exception as exc: log.warning("Inference error: %s", exc) raise HTTPException( status_code=502, detail=f"Error de inferencia: {str(exc)[:200]}", ) from exc # Mount static files al final: las rutas /api/* ya están registradas y tienen prioridad. app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static")