Spaces:
Sleeping
Sleeping
| # PromptCraft / Prompteador — Informe de arquitectura (KB) | |
| **Fecha**: 2026-07-02/03 · **Repo**: `huggingface.co/spaces/Malaji71/Prompteador` · **Clon local**: `~/Desktop/Prompteador` | |
| **Rango de commits de esta reconstrucción**: `f467051` (estado heredado) → `22f26d9` (índice 100k) | |
| Documento de referencia para sesiones futuras: qué había, qué se cambió y por qué, con los nombres exactos de clases, funciones y constantes. | |
| --- | |
| ## 1. Estado inicial (lo que había) | |
| Aplicación Gradio en un Space privado de HF que refinaba prompts de imagen desde castellano. Estructura plana: `app.py`, `agent.py`, `semantic_database.py`, `faiss_index.bin`, `metadata.json`. | |
| ### 1.1 Componentes originales | |
| - **`app.py` — `LlamaRefiner`**: pipeline de 1 sola llamada LLM. Traducción ES→EN con `Helsinki-NLP/opus-mt-es-en` (Inference API), recuperación FAISS, y una llamada a `Qwen/Qwen2.5-7B-Instruct:together` (vía router `https://router.huggingface.co/v1`) que debía hacer dos trabajos incompatibles a la vez: creatividad con el léxico y formato estricto. | |
| - **`app.py` — `SDXLGenerator`**: generación de imagen con `stabilityai/stable-diffusion-xl-base-1.0` vía Inference API. *(Eliminado después por decisión del dueño: caro y de mala calidad.)* | |
| - **`agent.py` — `ImprovedSemanticAgent`**: FAISS (`IndexFlatIP`) + `BAAI/bge-small-en-v1.5`. Cargaba un `CrossEncoder` (`cross-encoder/ms-marco-MiniLM-L6-v2`) que **nunca se usaba** (~90 MB de RAM muertos). El "patrón estructural" era un conteo de comas que elegía entre **4 plantillas fijas** con etiquetas tipo `Opening/Pose → Subject/Core → ...`. | |
| - **`semantic_database.py`**: 541 líneas de caché SQLite que **nadie importaba** (código muerto). Eliminado. | |
| - **README**: corrupto — contenía un transcript de chat pegado ("¡Claro, socio!...") y documentaba un modelo equivocado (LLaMA-3.2 cuando el código usaba Qwen). | |
| ### 1.2 Bugs y problemas del estado inicial | |
| 1. **Bug principal (`too_long`)**: el system prompt mostraba al LLM el template con etiquetas; Qwen las copiaba en el output ("Opening/Pose: ..."), `_clean_output` no las quitaba, el conteo de palabras se inflaba y el validador (umbral 40 palabras) rechazaba → fallback semántico pobre casi siempre. | |
| 2. **Filtro de léxico contradictorio**: solo pasaban fragmentos de 5–15 caracteres, pero las `SAFE_ATMOSPHERIC_FRAGMENTS` medían 25–44 → la priorización de frases seguras era inalcanzable. | |
| 3. **Categorías ficticias**: la UI ofrecía categorías y el código filtraba por `ex.get('category')` — **ese campo nunca existió en `metadata.json`** (claves reales: `caption`, `field` constante, `example_id`). Con categoría manual seleccionada, el filtro devolvía 0 candidatos. | |
| 4. **Índice incompleto y con receta inconsistente**: `faiss_index.bin` contenía **solo 10.000 vectores** de los 100.000 captions del metadata (el log "100,000 ejemplos cargados" contaba el JSON). Además, re-embeber esos mismos captions con bge-small daba cosenos de 0.75–0.89 contra los vectores almacenados → el índice original se construyó con **otra receta** que no casaba con la query. | |
| 5. **Requirements sin wheels para Python 3.13** (`sentencepiece>=0.1.99` podía resolver a 0.2.0 sin wheel cp313), falta de `ssr_mode=False`, sin prefijo BGE en queries. | |
| --- | |
| ## 2. Arquitectura actual: la Y (tri-LLM) con profundidad adaptativa | |
| ### 2.1 Flujo completo | |
| ``` | |
| input del usuario (ES o EN) | |
| │ | |
| ├─ LlamaRefiner._looks_english() → si es inglés, opus-mt se salta | |
| ├─ LlamaRefiner._translate_to_english() → opus-mt-es-en, con _translation_cache | |
| │ | |
| ├─ y_refiner.is_elaborate(user_en) → régimen: | |
| │ compose (idea simple) | polish (prompt ya trabajado) | |
| │ | |
| ├─ ImprovedSemanticAgent.extract_pattern_and_lexicon(user_en, k=6, explore=bool(variación)) | |
| │ ├─ clasificación interna de la query (CATEGORY_PROTOTYPES, margen ≥ CATEGORY_MARGIN=0.03) | |
| │ ├─ FAISS top-50 → filtro por etiqueta auto-generada (si detección confiada y ≥8 vecinos) | |
| │ ├─ CrossEncoder rerank (30 pares → top-15) [con _retrieval_cache por concepto] | |
| │ ├─ selección: MMR λ=0.7 (1ª generación) | muestreo ponderado por rango (regeneraciones) | |
| │ └─ _grammar_profile(examples) → spec gramatical medida (spaCy por cláusula) | |
| │ | |
| ├─ EN PARALELO (ThreadPoolExecutor, max_workers=2): | |
| │ α zai-org/GLM-5.2:fireworks-ai → canónico: spec + léxico (solo α ve el RAG) | |
| │ γ moonshotai/Kimi-K2.6:fireworks-ai → concreto: SOLO el concepto, quality-tags prohibidos | |
| │ | |
| ├─ N deepseek-ai/DeepSeek-V4-Flash:fireworks-ai → compilador (compose) o editor (polish) | |
| │ | |
| └─ contrato (YRefiner._check_contract) → cadena de degradación: | |
| N intento 1 → N intento 2 → α/γ directo (y_direct) → 1 LLM (degraded_fast) → fallback semántico | |
| ``` | |
| ### 2.2 `y_refiner.py` — nombres clave | |
| - **`MODEL_REGISTRY`**: dict por rol (`alpha`, `gamma`, `normalizer`, `fast`) con cadenas `[primario, fallback...]`, `pricing` (USD/1M in-out) y `extra` (params de body por modelo). | |
| - **`NODE_PROMPTS`**: 5 system prompts — `alpha`, `gamma`, `normalizer` (modo componer) + `alpha_polish`, `normalizer_polish` (modo pulir). γ no tiene variante polish (nunca ve el dataset). | |
| - **`call_with_fallback(client, role, messages, max_tokens, temperature, timeout)`**: recorre la cadena del rol. 1 retry con backoff solo en transitorios (429/5xx/timeout); fallos deterministas (4xx, respuesta vacía `_EmptyResponse`) saltan al siguiente modelo. **Circuit breaker de sesión** (`_BREAKER_THRESHOLD=3` fallos consecutivos → entrada saltada hasta que vuelva a responder). Si un provider rechaza `extra` con 400 "Extra inputs", reintenta sin ellos. | |
| - **`is_elaborate(text_en)`**: detector de régimen — `(≥25 palabras y ≥3 cláusulas) o ≥2 CRAFT_TERMS o ≥40 palabras`. `CRAFT_TERMS` = vocabulario de oficio visual ("golden hour", "85mm", "depth of field"...). | |
| - **`VARIATION_ANGLES`**: 8 lentes (encuadre/luz/tono) que rotan en regeneraciones. | |
| - **`YRefiner`**: | |
| - `refine_y(prompt_es, category="auto", progress_cb=None)` — pipeline principal. | |
| - `self.seen` — memoria anti-repetición por concepto (clave = texto normalizado); `variation = len(seen[key])` decide ángulo y temperaturas (compose: α/γ 0.7→0.95, N 0.35→0.75; polish: N 0.5→0.8, reintento SIEMPRE más caliente). | |
| - `_concept_block()` — adjunta el **castellano original** a los 3 nodos (`ORIGINAL (Spanish): ...`) para reparar artefactos de opus-mt. | |
| - `_check_contract(user_en, raw, polish)` — limpieza + validación; en polish añade el guardarraíl **`no_edit`**: jaccard de palabras output/input > 0.75 = incumplimiento (la edición nula era el óptimo del contrato de fidelidad v1 — lección clave). | |
| - Degradación parcial: si cae α o γ, el superviviente ocupa ambos slots de N (la prohibición "no copies verbatim" fuerza reescritura). | |
| - Telemetría: `self.session` (agregados: `n_ok_first/retry`, `y_direct`, `degraded_fast`, `fallback_sem`, `cost_total_usd`) + `self.records` (deque 200: latencias por nodo, `failed_node`, `final_step`, `depth`, `category`, coste). | |
| - **`check_catalog()`**: al arrancar, 1 GET a `router.huggingface.co/v1/models` y log de qué primarios siguen live (mitigación de desaparición de providers). | |
| - **Constantes**: `TIMEOUTS` (α/γ 30s, N 20s), `GLOBAL_BUDGET_S=75`, `MAX_TOKENS=300` los tres nodos (160 truncaba pulidos largos). | |
| ### 2.3 `agent.py` — nombres clave | |
| - **`BGE_QUERY_PREFIX`**: `"Represent this sentence for searching relevant passages: "` — SOLO en queries; los documentos se embebieron sin prefijo. | |
| - **Auto-etiquetado del índice** (en `_lazy_init`): `CATEGORY_PROTOTYPES` (10 categorías: portrait, landscape, urban, object, fantasy, animal, interior, food, graphic, text; 1–2 frases prototipo cada una) → se embeben los prototipos, se leen los vectores YA almacenados con `index.reconstruct_n` (por bloques de 100k) y `caption_labels = argmax(vecs @ protos.T)`. Instantáneo, sin re-embeber. Distribución del índice en producción: ver §3.1. | |
| - **Clasificación de query**: embedding sin prefijo vs prototipos; si `margen top1−top2 ≥ CATEGORY_MARGIN (0.03)` → filtra los hits FAISS a esa etiqueta (mínimo 8 vecinos o se relaja). Si es ambiguo (dragón: ¿fantasy o animal?) **se abstiene** → recuperación semántica pura. | |
| - **`_retrieval_cache`**: `(concepto normalizado) → (query_emb, top-15 rerankeado, categoría)`. Las regeneraciones solo re-muestrean (agente: ~3s → ~0.5s). | |
| - **`_grammar_profile(examples)`**: mide la sintaxis real del cluster — cláusulas partidas por `[,;.]`, mediana de cláusulas (4–8), y por slot posicional: forma dominante (`_analyze_clause` con spaCy → `SHAPE_DESCRIPTIONS`: NP/ING/PP/ADJ/SENT), longitud media y una cláusula real de muestra (que debe coincidir con la forma elegida). Sustituye a las 4 plantillas fijas. | |
| - **`_mmr_select(query_emb, candidates, k, lam=0.7)`**: Maximal Marginal Relevance con embeddings del propio índice (reconstruct; fallback re-encode). | |
| - **`_filter_and_score`**: fragmentos de léxico 6–48 chars, sin conjunción inicial, sin meta-lenguaje de caption ("this image...", "photo"), sin PROPN largos, contra `CONTENT_BLACKLIST`. | |
| - **Warmup** en `_lazy_init` (encode + rerank dummy) — el primer usuario ya no paga ~10s de carga. | |
| ### 2.4 `app.py` — nombres clave | |
| - **`LlamaRefiner`** (conserva el nombre histórico; hoy es la infraestructura compartida + el escalón `fast`): | |
| - `openai_client = OpenAI(base_url=router, max_retries=0)` — **crítico**: los retries internos del SDK se apilaban con nuestra cadena y un nodo caído tardaba ~90s en fallar. | |
| - `_translate_to_english` con `_translation_cache` y `_looks_english` (heurística de stopwords EN vs ES). | |
| - `_clean_output`: strip de bloques `<think>` (y apertura truncada), prefijos ("Here is"), **`_SECTION_LABEL_RE`** (etiquetas de 1 o 2 palabras: "Opening/Pose:", "Secondary Detail:", "Environmental Frame:"), flechas→comas, bullets, comas dobles/colgantes, y cadencia de fragmentos (". " entre oraciones → ", ", sin punto final). | |
| - `_validate_output`: mínimo 10 chars, concepto presente (palabras >3 letras del user_en), términos prohibidos. **SIN límite de longitud** (decisión del dueño: los encoders modernos no se degradan; se eliminó `too_long`). | |
| - `_call_llm` usa `call_with_fallback(role="fast")` — el modo rápido también tiene cadena de fallback. | |
| - **UI mínima**: textbox de idea + botón "Refinar prompt" + prompt refinado + referencia estructural + estado/métricas. Sin categorías, sin proporciones, sin generador de imagen, sin banderas ni emojis en labels. Cabecera: solo "PromptCraft v3.1 / Compositional Transposition Engine." | |
| ### 2.5 Modelos y precios (verificados en el router, 2026-07-02) | |
| | Rol | Primario | Fallbacks | Precio in/out por 1M | | |
| |---|---|---|---| | |
| | α | `zai-org/GLM-5.2:fireworks-ai` | GLM-5.1:together → GLM-4.7:deepinfra | $1.40/$4.40 | | |
| | γ | `moonshotai/Kimi-K2.6:fireworks-ai` | Llama-3.3-70B:groq → Kimi-K2.5:fireworks → Kimi-K2.6:together (último: no honra el flag anti-thinking) | $0.95/$4.00 | | |
| | N | `deepseek-ai/DeepSeek-V4-Flash:fireworks-ai` | Qwen3.5-9B:deepinfra → Qwen2.5-7B:together | $0.14/$0.28 | | |
| | fast | `Qwen/Qwen2.5-7B-Instruct:together` | Qwen3.5-9B:deepinfra | $0.30/$0.30 | | |
| **Coste real medido**: ~$0.0013–0.003 por refinamiento Y (sube con anti-repetición/pulido por más tokens de input). Con los $2/mes de créditos Pro: ~700–1.500 refinamientos/mes. **Latencia**: primera generación ~8–15s; regeneraciones ~4–8s (cachés). | |
| --- | |
| ## 3. Dataset e índice | |
| ### 3.1 RAG v2 — el índice en producción (commit `020c862`, 2026-07-03) | |
| **Arquitectura de bucket**: el índice vive FUERA del Space, en el repo dataset **privado** `Malaji71/prompteador-rag`: | |
| - `index_v2.faiss` — 706 MB, IndexFlatIP **459.718 × 384**, bge-small-en-v1.5 normalizado sin prefijo. | |
| - `captions_v2.parquet` — 228 MB, columnas `caption` + `source_prompt`. | |
| El Space los descarga al arrancar (`ImprovedSemanticAgent._load_rag_v2()`, env `RAG_REPO`, default `Malaji71/prompteador-rag`) con `hf_hub_download` y el secreto `HF_TOKEN`. **La descarga intra-datacenter tarda ~3 segundos** (medido: 318 MB/s) — el bucket no cuesta tiempo de arranque. Fallback automático al índice local del repo si el bucket no responde. | |
| **Fuente**: `Photoroom/midjourney-v6-recap` (marzo 2026, MIT) — 1,23M imágenes **Midjourney v6** re-capturadas por 3 VLMs (`llava`, `gemini`, `qwen3`). Se usa la columna **`qwen3`** (Qwen3-VL, la de mayor calidad). Clave del enfoque: las recaptions VLM describen la imagen, así que **por construcción no contienen nombres de artistas ni parámetros MJ** (requisito ético del dueño), y declaran el medio ("photograph" vs "painting") — lo que da un filtro de fotorealismo gratuito. | |
| **Curación** (1,23M → 459.718, ~52% de supervivencia, validada primero sobre muestra de 300 vía datasets-server): | |
| 1. `qwen3` no nulo y >150 chars. | |
| 2. Fotorealismo: descartar si las primeras ~200 chars declaran medio artístico (painting/illustration/anime/3d render/...) sin marcador fotográfico. | |
| 3. Veto temático (requisito del dueño: NO fantasy/cyberpunk): cyberpunk, dystopian, dragon, elf, wizard, sci-fi, futuristic, robot, zombie, superhero... | |
| 4. Cero referencias de estilo: patrón `in the style of` / `style of [A-Z]`. | |
| 5. Longitud 40-160 palabras; dedup exacto normalizado; máximo 2 variantes por prompt original (el dataset trae variantes de grid de un mismo prompt). | |
| **Distribución auto-etiquetada v2**: object 26%, portrait 20%, interior 15%, landscape 14%, animal 12,5%, urban 10%, food 1,3%, fantasy 0,6%, graphic 0,2%, text 0,1%. (Las categorías débiles del v1 —animal 3%, urban 3,6%— ahora tienen masa real.) | |
| **Build**: HF Jobs `t4-small`, `--timeout 2h` (~45 min reales). El parquet fuente pesa **193 GB** (imágenes embebidas) — imprescindible leer solo columnas de texto con **DuckDB sobre `hf://`** (proyección de columnas → ~2 GB transferidos). El job crea el repo destino (`create_repo(exist_ok=True)`) y sube con `upload_file`. Script: `build_rag_v2.py` (PEP-723 inline deps: sentence-transformers, faiss-cpu, duckdb, pyarrow, torch). | |
| **Datasets descartados en el scouting** (para no repetir la evaluación): `vivym/midjourney-prompts` (9M pero era v5 — obsoleto según el dueño), `suriyagunasekar/midjourney-prompts` (2026 pero prompts crudos de Discord: artistas, celebrities, --params, cyberpunk — todo lo vetado), `Ashenone3/Midjourney-23M` (webdataset con imágenes, mixto). | |
| ### 3.2 Ficheros legacy en el repo del Space (solo fallback) | |
| - **`metadata.json`** (100k, LFS): `{"caption", "field" (constante), "example_id"}`. Sin etiquetas de categoría. | |
| - **`faiss_index.bin`** (147 MB, LFS, commit `22f26d9`): IndexFlatIP 100k × 384, reconstruido en T4 (el índice pre-existente tenía solo 10k vectores y otra receta de embedding, cosenos 0.75-0.89 vs re-embedding). Candidatos a borrarse del repo del Space cuando el bucket se considere estable. | |
| --- | |
| ## 4. Decisiones de diseño y lecciones (con contexto) | |
| 1. **`reasoning_effort: "none"` es obligatorio** para los híbridos 2026 del router (GLM-5.2, Kimi-K2.6/2.5, DeepSeek-V4, Qwen3.5): sin él, el razonamiento inline consume TODO el max_tokens y `content` sale vacío o lleno de deliberación. El router **rechaza** `chat_template_kwargs` y `reasoning.enabled` (400 "Extra inputs"). **Kimi-K2.6 servido por together no honra el flag** (content vacío siempre) — por eso γ va por fireworks y together quedó al final de su cadena. | |
| 2. **`max_retries=0` en el cliente OpenAI**: los reintentos son de `call_with_fallback`, no del SDK — si no, se multiplican. | |
| 3. **Sin límite de longitud**: FLUX/MJ/SD3 (T5/LLM encoders) no se degradan con prompts largos. `too_long` eliminado del validador; la cadencia la marca la spec gramatical, no un techo. | |
| 4. **Fidelidad ≠ identidad** (lección del modo pulido v1): un contrato que dice "conserva la redacción, no pierdas nada" tiene como óptimo la **edición nula** — el sistema devolvía el input intacto. Arreglo: se protege el *inventario* (objetos/acciones/relaciones/tono) y el orden, nunca la redacción; guardarraíl `no_edit` (jaccard >0.75) y reintento a temperatura MÁS alta. Regla general: *cada regla nueva en un prompt puede tener un óptimo degenerado que nadie pidió*. | |
| 5. **Clasificación con abstención**: filtrar por categoría solo con margen ≥0.03; en ambigüedad real (dragón, taza de café) mejor no filtrar que filtrar mal. | |
| 6. **El castellano original viaja a los nodos**: opus-mt (2020, pequeño) comete errores ("norays" sin traducir, "pescadora"→"fisherman", "bicycle horn"→"bike horny" al recibir inglés); GLM/Kimi/DeepSeek entienden castellano y reparan con el original como fuente de verdad. *Deuda conocida*: opus-mt es candidato a jubilación total (traducir con uno de los LLMs del trío o pasar castellano directo; solo FAISS necesita inglés). | |
| 7. **Variación de regeneraciones** = 4 palancas juntas: muestreo estocástico del top-15 (`explore=True`), ángulo rotado (`VARIATION_ANGLES`), anti-repetición (`seen`, hasta 3 versiones previas inyectadas a α/γ/N), temperaturas altas. Resultado medido: Jaccard entre regeneraciones 0.19–0.43 (antes ~0.85). | |
| --- | |
| ## 5. Gotchas operativos (para no re-descubrirlos) | |
| - **Probar el router con `urllib` da 403 de Cloudflare** (bloqueo por user-agent de Python). Con el SDK de OpenAI funciona. No es problema de cuenta ni de provider. | |
| - **HF Jobs requiere token con permiso `job.write`** — el token fine-grained cacheado en `~/.cache/huggingface/token` no lo tiene; el dueño tiene uno aparte con Jobs habilitado. | |
| - **Gradio 6**: `show_copy_button` ya no existe en `Textbox` (crasheó un deploy). `ssr_mode=False` en `launch()` evita "Could not get API info". | |
| - **`huggingface_hub>=1.0` rompe `transformers`** (exige <1.0) — cuidado al actualizar el hub para usar `hf jobs` en un entorno compartido con el runtime del Space. | |
| - **Python 3.13 en Spaces**: `sentencepiece` solo tiene wheel cp313 en 0.2.1 exacto; spacy ≥3.8.2; torch ≥2.6. El Mac local es Intel (torch macOS x86_64 muere en 2.2.2) — los tests locales van en venv del scratchpad con `--system-site-packages`. | |
| - **Logs del Space**: httpx silenciado a WARNING y `show_progress_bar=False` en todos los encode/predict — si vuelven las 40 líneas por refinamiento, algo re-activó el logger. | |
| - **El RAG v2 es deliberadamente débil en fantasy (0,6%), graphic (0,2%) y text (0,1%)** — decisión del dueño (interés: fotorealismo). Conceptos de esas categorías caen en recuperación semántica sin filtro (el mínimo de 8 vecinos relaja el filtro solo) y el léxico será menos específico. Si algún día se quiere cubrir ilustración/fantasy, sería un índice aparte, no contaminar este. | |
| - **Leer datasets HF con imágenes embebidas**: comprobar SIEMPRE el peso real con `datasets-server.huggingface.co/size` antes de `load_dataset` — `Photoroom/midjourney-v6-recap` pesa 193 GB. La proyección de columnas (DuckDB `hf://` o pyarrow) reduce la lectura a solo las columnas pedidas. | |
| - **Muestrear sin descargar**: `datasets-server.huggingface.co/rows?dataset=...&offset=N&length=100` permite validar filtros de curación sobre muestras antes de gastar GPU. | |
| --- | |
| ## 6. Historial de commits de la reconstrucción | |
| | Commit | Contenido | | |
| |---|---| | |
| | `35caeb4` | Arquitectura Y inicial + fix bug `too_long` + BGE prefix + reranker activado + MMR + requirements py3.13 + README limpio + borrado `semantic_database.py` | | |
| | `a4ac9b2` | Y como único motor (sin toggle), cabecera mínima | | |
| | `5faf28e` | γ primario a fireworks (together no honra `reasoning_effort`) | | |
| | `7d8aef4` | Etiquetas de 2 palabras en `_clean_output`, léxico sin basura, escalón `y_direct` | | |
| | `8092b99`/`159c513` | Fix crash Gradio 6 (`show_copy_button`), UI sin banderas/emojis | | |
| | `018e73b` | v3.2: spec gramatical medida (spaCy), variación en regeneraciones, sin generador SDXL, sin categorías manuales, sin límite de longitud | | |
| | `03bfd54` | `max_retries=0`, caché de traducción, caché de recuperación, logs limpios | | |
| | `97e6f60` | Profundidad adaptativa (`is_elaborate`, prompts `*_polish`, castellano a los nodos) | | |
| | `5450bb6` | Guardarraíl `no_edit`, max_tokens 300, detección de inglés | | |
| | `e9a071c` | Auto-etiquetado del índice + clasificación interna de queries con abstención | | |
| | `22f26d9` | **Índice reconstruido: 100k vectores en GPU (HF Jobs/T4), receta consistente** | | |
| | `675b76b` | ARCHITECTURE.md (este documento) | | |
| | `020c862` | **RAG v2: 459.718 captions fotorealistas MJ v6 desde el repo-bucket `Malaji71/prompteador-rag`** | | |
| --- | |
| ## 7. Cómo verificar (patrones de test usados) | |
| - **Unit del contrato**: mockear `gradio/openai/huggingface_hub/agent/y_refiner` en `sys.modules`, instanciar `LlamaRefiner` con `object.__new__`, y ejercitar `_clean_output`/`_validate_output` con los casos canónicos: etiquetas 1 y 2 palabras, flechas, `<think>`, "Here is", comas colgantes, frases con punto. | |
| - **Integración local**: venv del scratchpad (`--system-site-packages` + spacy), `HF_TOKEN` del cache, `cwd` en el repo (FAISS relativo). Medir: `final_step`, latencias por nodo (`y.records[-1]['lat']`), jaccard entre regeneraciones (<0.5), preservación de inventario en polish (>85% de content words), coste (`y.session['cost_total_usd']`). | |
| - **Degradación**: sustituir temporalmente `MODEL_REGISTRY[rol]` por un modelo inexistente y verificar el escalón esperado. | |
| - **Router**: catálogo en `https://router.huggingface.co/v1/models` (público) — precios, providers live, ttft y throughput por provider. | |