Spaces:
Sleeping
A newer version of the Gradio SDK is available: 6.23.1
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 conHelsinki-NLP/opus-mt-es-en(Inference API), recuperación FAISS, y una llamada aQwen/Qwen2.5-7B-Instruct:together(vía routerhttps://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 constabilityai/stable-diffusion-xl-base-1.0ví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 unCrossEncoder(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 tipoOpening/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
- Bug principal (
too_long): el system prompt mostraba al LLM el template con etiquetas; Qwen las copiaba en el output ("Opening/Pose: ..."),_clean_outputno las quitaba, el conteo de palabras se inflaba y el validador (umbral 40 palabras) rechazaba → fallback semántico pobre casi siempre. - Filtro de léxico contradictorio: solo pasaban fragmentos de 5–15 caracteres, pero las
SAFE_ATMOSPHERIC_FRAGMENTSmedían 25–44 → la priorización de frases seguras era inalcanzable. - Categorías ficticias: la UI ofrecía categorías y el código filtraba por
ex.get('category')— ese campo nunca existió enmetadata.json(claves reales:caption,fieldconstante,example_id). Con categoría manual seleccionada, el filtro devolvía 0 candidatos. - Índice incompleto y con receta inconsistente:
faiss_index.bincontení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. - Requirements sin wheels para Python 3.13 (
sentencepiece>=0.1.99podía resolver a 0.2.0 sin wheel cp313), falta dessr_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) yextra(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=3fallos consecutivos → entrada saltada hasta que vuelva a responder). Si un provider rechazaextracon 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ílno_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 arouter.huggingface.co/v1/modelsy log de qué primarios siguen live (mitigación de desaparición de providers).- Constantes:
TIMEOUTS(α/γ 30s, N 20s),GLOBAL_BUDGET_S=75,MAX_TOKENS=300los 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 conindex.reconstruct_n(por bloques de 100k) ycaption_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_clausecon 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, contraCONTENT_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ónfast):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_englishcon_translation_cachey_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_llmusacall_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, columnascaption+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):
qwen3no nulo y >150 chars.- Fotorealismo: descartar si las primeras ~200 chars declaran medio artístico (painting/illustration/anime/3d render/...) sin marcador fotográfico.
- Veto temático (requisito del dueño: NO fantasy/cyberpunk): cyberpunk, dystopian, dragon, elf, wizard, sci-fi, futuristic, robot, zombie, superhero...
- Cero referencias de estilo: patrón
in the style of/style of [A-Z]. - 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, commit22f26d9): 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)
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 ycontentsale vacío o lleno de deliberación. El router rechazachat_template_kwargsyreasoning.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.max_retries=0en el cliente OpenAI: los reintentos son decall_with_fallback, no del SDK — si no, se multiplican.- Sin límite de longitud: FLUX/MJ/SD3 (T5/LLM encoders) no se degradan con prompts largos.
too_longeliminado del validador; la cadencia la marca la spec gramatical, no un techo. - 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ó. - 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.
- 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).
- 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
urllibda 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/tokenno lo tiene; el dueño tiene uno aparte con Jobs habilitado. - Gradio 6:
show_copy_buttonya no existe enTextbox(crasheó un deploy).ssr_mode=Falseenlaunch()evita "Could not get API info". huggingface_hub>=1.0rompetransformers(exige <1.0) — cuidado al actualizar el hub para usarhf jobsen un entorno compartido con el runtime del Space.- Python 3.13 en Spaces:
sentencepiecesolo 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=Falseen 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/sizeantes deload_dataset—Photoroom/midjourney-v6-recappesa 193 GB. La proyección de columnas (DuckDBhf://o pyarrow) reduce la lectura a solo las columnas pedidas. - Muestrear sin descargar:
datasets-server.huggingface.co/rows?dataset=...&offset=N&length=100permite 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_refinerensys.modules, instanciarLlamaRefinerconobject.__new__, y ejercitar_clean_output/_validate_outputcon 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_TOKENdel cache,cwden 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.