Prompteador / ARCHITECTURE.md
Malaji71's picture
Trivium: identidad nueva, interfaz clara, arquitectura visible
4776660
|
Raw
History Blame Contribute Delete
21.8 kB
# 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.