Prompteador / ARCHITECTURE.md
Malaji71's picture
Trivium: identidad nueva, interfaz clara, arquitectura visible
4776660
|
Raw
History Blame Contribute Delete
21.8 kB

A newer version of the Gradio SDK is available: 6.23.1

Upgrade

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.pyLlamaRefiner: 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.pySDXLGenerator: 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.pyImprovedSemanticAgent: 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_datasetPhotoroom/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.