Efradeca commited on
Commit
4d2cc2d
·
verified ·
1 Parent(s): b9d47a7

chore: deploy private lightloom build

Browse files
.gitignore ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ .env.*
3
+ !.env.example
4
+ *.token
5
+ *.secret
6
+ secrets/
7
+ weights/
8
+ models/
9
+ checkpoints/
10
+ *.safetensors
11
+ *.ckpt
12
+ *.pt
13
+ *.pth
14
+ *.onnx
15
+ *.gguf
16
+ *.zip
17
+ *.7z
18
+ *.tar
19
+ *.tar.gz
20
+ *.mp4
21
+ *.mov
22
+ *.wav
23
+ *.flac
24
+ *.png
25
+ *.jpg
26
+ *.jpeg
27
+ *.webp
28
+ *.log
29
+
30
+ .venv/
31
+ __pycache__/
32
+ *.py[cod]
33
+ .pytest_cache/
34
+ .mypy_cache/
35
+ .ruff_cache/
36
+ .coverage
37
+ htmlcov/
38
+
39
+ benchmarks/results/*
40
+ !benchmarks/results/.gitkeep
41
+ !benchmarks/results/*.json
42
+ !benchmarks/results/*.md
43
+
44
+ assets/showcase/*
45
+ !assets/showcase/.gitkeep
46
+ assets/anchors/generated/*
47
+ frontend/.cache/
48
+
49
+ vonfig.py
50
+ lightloom-docs/
AGENTS.md ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AGENTS.md — Lightloom (Build Small Hackathon)
2
+
3
+ You are the coding agent building **Lightloom**, a voice-driven live-cinema Gradio app for the
4
+ Hugging Face "Build Small" hackathon (deadline **June 15, 2026**). This file is your contract.
5
+ Read `MASTER_PLAN.md` and `docs/` before writing code. Work in English (code, comments, commits);
6
+ user-facing UI copy is bilingual ES/EN per `docs/04_UI_SPEC.md`.
7
+
8
+ ---
9
+
10
+ ## 1. HARD RULES (violating any of these disqualifies the project — never trade them for features)
11
+
12
+ R1. **Parameter cap:** total parameters of ALL runtime models ≤ 32B. Source of truth:
13
+ `src/lightloom/compliance/params_ledger.py` (every model must be registered there;
14
+ CI fails if the sum exceeds 32e9 or if a loaded model is missing from the ledger).
15
+ R2. **Gradio Space:** the deliverable is a Gradio app (`gradio.Server`) hosted as a Space under
16
+ the `build-small-hackathon` org. Never replace Gradio with a bare FastAPI/Node server.
17
+ R3. **Off the Grid:** ZERO network calls to external APIs at runtime. All model weights load
18
+ inside the Space. `tests/test_compliance.py::test_no_cloud_apis` greps runtime code for
19
+ forbidden patterns (`api.openai.com`, `api.cohere`, `bfl.ai/api`, `modal.com`, `requests.`/
20
+ `httpx.` to non-localhost, `InferenceClient`, `huggingface_hub.InferenceApi`). Downloading
21
+ weights from the HF Hub at startup IS allowed (it is model loading, not an inference API).
22
+ R4. **Honesty:** never present mocked output as live generation. The Showcase gallery is always
23
+ labeled "pre-rendered". Merit badges are claimed in the README ONLY if literally true
24
+ (see R6). No fabricated benchmark numbers — every figure in README/blog comes from a
25
+ committed benchmark script output.
26
+ R5. **Submission artifacts:** Space README.md must contain: GitHub repo link, demo video link,
27
+ social post link, parameter ledger table, license table, badge evidence links. Block the
28
+ final release if any is missing (`scripts/release_check.py`).
29
+ R6. **Badge truth conditions:** Off-the-Grid = R3 holds. Well-Tuned = our LoRA published on the
30
+ Hub AND loaded at runtime. Off-Brand = custom `gr.Server` frontend shipped. Llama-Champion =
31
+ at least one runtime model actually served through llama.cpp in the shipped app (if gate G3
32
+ forces the Director onto GPU transformers AND Aya is disabled, DO NOT claim this badge).
33
+ Sharing-is-Caring = director traces dataset published. Field-Notes = blog post published.
34
+ R7. **Codex attribution (OpenAI Track):** all code changes go through Codex with its default
35
+ attribution preserved. NEVER squash, rebase-rewrite, or re-author commits in a way that
36
+ strips Codex attribution. Public GitHub repo from commit #1.
37
+ R8. **Copyright:** demo narration texts are original or public domain (pre-1900 poetry OK).
38
+ No song lyrics, no copyrighted characters in prompts, no brand logos in UI.
39
+
40
+ ## 2. AGENT ROLES (operate as one role per session; declare the role in the first commit of the session)
41
+
42
+ - **R-PIPE (Pipeline Engineer):** owns `src/lightloom/{audio_in,director,translate,paint,depth,
43
+ sound,core}`. DoD: unit tests green, latency within budget (docs/02 §6), gates G1–G5 scripts.
44
+ - **R-FRONT (Frontend Cinematographer):** owns `frontend/` + `app.py` route wiring. DoD:
45
+ UI spec docs/04 implemented pixel-true, reduced-motion toggle works, judge-sim passes.
46
+ - **R-MLOPS (Trainer):** owns `training/` (Modal LoRA job, captions, publish-to-Hub script).
47
+ DoD: LoRA repo public on Hub with model card, loadable by `paint/lora.py`.
48
+ - **R-QA (QA & Compliance Auditor):** owns `tests/`, `benchmarks/`, `scripts/release_check.py`.
49
+ Adversarial mindset: try to break rules R1–R8 and the demo. DoD: docs/06 checklists executed.
50
+ - **R-REL (Release Manager):** owns Space config, README, publishing traces/blog, submission.
51
+ DoD: docs/08 submission package complete, dry-run as judge in incognito.
52
+
53
+ Role boundaries are soft for trivial fixes but PRs that mix roles must say why.
54
+
55
+ ## 3. PREFLIGHT (run BEFORE any feature work — `scripts/preflight.py` must print ALL GREEN)
56
+
57
+ P1. `huggingface_hub.whoami()` → user is a member of org `build-small-hackathon`.
58
+ P2. Can create/access the Space `build-small-hackathon/lightloom` (or agreed name); SDK=gradio;
59
+ hardware shows ZeroGPU available; record actual daily quota observed.
60
+ P3. Secrets present in Space settings: `HF_TOKEN` (read). No other secrets needed at runtime.
61
+ P4. Local/dev: Python ≥3.10; `pip install -U "git+https://github.com/huggingface/diffusers"`,
62
+ `transformers>=5.4.0`, `gradio[mcp]` latest, `llama-cpp-python` (prebuilt CPU wheel — if the
63
+ wheel build fails on the Space image, fall back to a pinned prebuilt wheel URL; log it).
64
+ `packages.txt` for the Space: `ffmpeg`.
65
+ P5. Resolve and pin EXACT model repo ids + revisions into `src/lightloom/core/config.py`
66
+ (candidates listed in docs/02 §1 with `VERIFY:` flags — confirm each card exists, note its
67
+ license string verbatim into the license table).
68
+ P6. Disk/cache: estimate total weight download size; if Space build-time download is too slow,
69
+ switch to lazy download on first request with a "warming up the projector" UI state.
70
+ P7. Confirm `import spaces` + `@spaces.GPU` works in a hello-world ZeroGPU Space before
71
+ porting the pipeline.
72
+
73
+ ## 4. CONVENTIONS
74
+
75
+ - Conventional commits (`feat:`, `fix:`, `bench:`, `docs:`, `compliance:`). Each D1 gate result
76
+ is committed as `bench(gateN): <metric>=<value>` with the raw script output in `/benchmarks/results/`.
77
+ - Every module has a docstring stating its parameter count contribution and whether it is
78
+ runtime or build-time.
79
+ - Anything not verified against a primary source is marked `# VERIFY:` and listed in
80
+ `docs/VERIFY_LOG.md` with resolution status. No `VERIFY:` may remain in the release.
81
+ - Config over constants: all knobs in `core/config.py` (resolution, steps, beat min/max seconds,
82
+ reference policy, quota guard thresholds).
83
+ - Errors degrade gracefully to UI states defined in docs/04 §5; never a raw stack trace on stage.
84
+
85
+ ## 5. DEFINITION OF DONE (release)
86
+
87
+ `scripts/release_check.py` green = R1–R8 verified + docs/06 E2E checklist + docs/08 submission
88
+ package complete + Space cold-start tested in incognito + Showcase mode reachable with GPU quota
89
+ exhausted (simulate by flag).
CLAUDE.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md — Lightloom (Claude Code entry point)
2
+
3
+ Read `AGENTS.md` first — it is the binding contract (hard rules R1–R8, roles, preflight,
4
+ conventions). Everything there applies verbatim. This file adds Claude Code–specific notes.
5
+
6
+ ## Claude Code specifics
7
+
8
+ 1. **Session = role.** Start each session by stating which role you are operating
9
+ (R-PIPE / R-FRONT / R-MLOPS / R-QA / R-REL) and which work package (WP-xx from
10
+ `docs/05_EXECUTION_PLAN.md`). Plan with TodoWrite-style task lists per WP; one WP per branch.
11
+ 2. **Codex attribution caveat (rule R7).** The OpenAI Track is judged on commits attributed to
12
+ Codex. If a change is authored via Claude Code instead, that is fine for the project but it
13
+ does NOT count toward the OpenAI Track. Strategy: use Codex for the bulk of authored commits;
14
+ use Claude Code for review, debugging, refactors, and QA — and never rewrite/squash existing
15
+ Codex-attributed history.
16
+ 3. **Long-running things you must NOT do inline:** ZeroGPU benchmarks (run them in the Space,
17
+ not locally), Modal LoRA training (kick off via `training/modal_lora/launch.py`, poll, never
18
+ block a session waiting), large weight downloads (background them, verify checksums after).
19
+ 4. **When uncertain about an API symbol** (e.g., the exact reference-image kwarg of the diffusers
20
+ Flux2 Klein KV pipeline), do not guess: open the installed package source under
21
+ `site-packages/diffusers/pipelines/flux2/` and read the signature, then remove the `VERIFY:`
22
+ tag with a commit citing the file/line.
23
+ 5. **Tests before features** for the Director: the GBNF grammar and SceneState reducer have unit
24
+ tests written first (they are pure CPU logic and the most fragile contract in the system).
25
+ 6. **Never** edit `benchmarks/results/*` by hand; results are only produced by running scripts.
26
+
27
+ ## Quick map
28
+ - Contract: `AGENTS.md` · Plan: `MASTER_PLAN.md` + `docs/05_EXECUTION_PLAN.md`
29
+ - Build order: F0 preflight → F1 gates → F2 pipeline → F3 frontend → F4 artifacts → F5 submission
30
+ - If behind schedule, apply cut order F6 (RIFE → ambient audio → Aya → LoRA). Ask before cutting.
MASTER_PLAN.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LIGHTLOOM — Plan Maestro de Construcción End-to-End
2
+ ### Build Small Hackathon (Hugging Face · Gradio) · Ventana: ahora → 15 jun 2026 · ~30 h efectivas
3
+
4
+ > Este paquete es la **fuente de verdad** para construir Lightloom con Codex o Claude Code.
5
+ > El agente de código lee `AGENTS.md` (Codex) / `CLAUDE.md` (Claude Code) como contrato.
6
+ > Los humanos leen este archivo primero y luego `docs/05_EXECUTION_PLAN.md`.
7
+
8
+ ---
9
+
10
+ ## 0. Qué es Lightloom (definición de producto)
11
+
12
+ **One-liner:** Habla — y cinco modelos diminutos ruedan tu película: te escuchan, te traducen, dirigen la fotografía, pintan cada plano con memoria, le dan profundidad y le ponen sonido. Sin nube. ~7B de 32B parámetros permitidos.
13
+
14
+ **Track:** Thousand Token Wood. **Forma:** Gradio app (`gradio.Server`, frontend custom) en un Space ZeroGPU dentro de la org `build-small-hackathon`.
15
+
16
+ **La tesis** (alineada con el "Chapter Zero" del evento): una orquesta de especialistas pequeños y componibles supera la experiencia de un monolito gigante. Lightloom es esa tesis convertida en producto.
17
+
18
+ ## 1. Objetivos y métricas de éxito (medibles)
19
+
20
+ | ID | Objetivo | Métrica de éxito | Verificación |
21
+ |---|---|---|---|
22
+ | O1 | Sensación de "película en vivo" | TTFF (time-to-first-frame) ≤ 8 s desde que empiezas a hablar; ≤ 2.5 s GPU/beat | `benchmarks/gate_g1.py` |
23
+ | O2 | Continuidad cinematográfica real | Coherencia de personaje/paleta a 12 cuadros encadenados (contact sheet aprobada) | `benchmarks/gate_g4.py` |
24
+ | O3 | Innovación legible para el jurado | La claqueta muestra decisión cut/continuity + cámara + paleta en < 5 s de mirada | Revisión E2E (docs/06) |
25
+ | O4 | Espectacularidad | Paralaje 2.5D activo en todo cuadro + transición + grano + soundbed | Demo checklist |
26
+ | O5 | Cumplimiento total de reglas | 0 APIs cloud en runtime; suma params ≤ 32B; Gradio Space; video+post en README | `tests/test_compliance.py` |
27
+ | O6 | Stacking de premios | Elegibilidad documentada en README para: TTW, OpenBMB, Cohere, OpenAI, Modal, Off-Brand, Best Demo, Bonus Quests | docs/08 checklist |
28
+ | O7 | Robustez de evaluación | App usable por juez sin login, sin mic (Recital), con cuota agotada (Showcase) | Judge-sim script |
29
+ | O8 | Artefacto compartible | Export MP4 con voz del usuario + storyboard PNG funcionando | Test E2E export |
30
+
31
+ ## 2. Restricciones duras (resumen — detalle en docs/08)
32
+
33
+ 1. **≤ 32B parámetros TOTALES** sumando todos los modelos del runtime. Ledger automático obligatorio.
34
+ 2. **App Gradio** alojada como **Space en la org del hackathon**. `gradio.Server` es Gradio (paquete oficial) — válido.
35
+ 3. **Video demo + post social** enlazados en el README del Space antes del cierre (15 jun).
36
+ 4. **Off the Grid (insignia objetivo):** cero llamadas a APIs externas en runtime. Modal/Codex solo en build-time.
37
+ 5. **Honestidad:** nada mock se presenta como funcional; Showcase mode siempre etiquetado; insignias solo si se cumplen de verdad.
38
+
39
+ ## 3. La orquesta (modelos del runtime — detalle y matemática en docs/02)
40
+
41
+ | Modelo | Rol | Params | Runtime |
42
+ |---|---|---:|---|
43
+ | Cohere Transcribe 03-2026 | Oídos (ASR, 14 idiomas) | 2.00B | transformers ≥5.4, GPU |
44
+ | Cohere Tiny Aya Global (gate G2) | Traductor (70+ idiomas → EN) | 3.35B | llama.cpp GGUF Q4, CPU |
45
+ | MiniCPM5-1B | **Director** (SceneState, montaje, cámara) | 1.00B | llama.cpp + GBNF (gate G3) |
46
+ | FLUX.2 [klein] 4B distilled | Pintor (4 pasos, multi-ref KV) | 4.00B | diffusers, ZeroGPU, FP8+AoT |
47
+ | Depth Anything V2 Small | Profundidad (paralaje 2.5D) | 0.025B | transformers, GPU |
48
+ | Stable Audio Open Small | Sonidista (ambientes, EN-only) | 0.341B | stable-audio-tools, GPU |
49
+ | RIFE 4.x lite | Interpolador (solo export MP4) | 0.010B | PyTorch, GPU |
50
+ | Silero VAD | Detector de beats | 0.002B | CPU |
51
+ | **TOTAL** | | **7.38B (10.73B c/ Aya)** | **de 32B** |
52
+
53
+ Build-time (no cuenta para runtime pero se declara): FLUX.2 klein **base** 4B para entrenar la LoRA en Modal.
54
+
55
+ ## 4. Mapa del paquete de documentación
56
+
57
+ | Archivo | Contenido | Lo consume |
58
+ |---|---|---|
59
+ | `AGENTS.md` | Contrato del agente Codex: reglas, roles, comprobaciones iniciales, convenciones | Codex |
60
+ | `CLAUDE.md` | Idem para Claude Code (delta sobre AGENTS.md) | Claude Code |
61
+ | `docs/01_ARCHITECTURE.md` | Arquitectura, estructura de carpetas, contratos de datos, diagramas mermaid, stack | Agente + humano |
62
+ | `docs/02_MODELS_AND_MATH.md` | IDs exactos de modelos, formulación matemática de cada componente, presupuestos | Agente + humano |
63
+ | `docs/03_PROMPTS.md` | System prompt del Director, gramática GBNF, plantillas de prompts, prompts del agente por WP | Agente |
64
+ | `docs/04_UI_SPEC.md` | Identidad visual, tokens de diseño, pantallas S0–S4, estados, flujos de usuario | Agente (frontend) |
65
+ | `docs/05_EXECUTION_PLAN.md` | Fases F0–F6, work packages, roles, gates D1, cronograma, orden de corte | Humano + agente |
66
+ | `docs/06_TESTING_QA.md` | Benchmarks ZeroGPU (gates), unit/integration/E2E, simulación de juez, presupuestos de rendimiento | Agente (QA) |
67
+ | `docs/07_RISKS.md` | Matriz de riesgos consolidada con mitigaciones y planes B | Humano |
68
+ | `docs/08_COMPLIANCE_HACKATHON.md` | Regla→enforcement, ledger de parámetros, licencias, paquete de submission (README/video/post/blog/trazas) | Agente + humano |
69
+
70
+ ## 5. Fases (resumen — detalle en docs/05)
71
+
72
+ - **F0 · Preflight (D1 mañana, 1 h):** comprobaciones iniciales automatizadas (org, Space, tokens, versiones, licencias). Sin verde aquí, no se construye nada.
73
+ - **F1 · Gates empíricos (D1, 4–5 h):** G1 latencia klein · G2 español MiniCPM · G3 tok/s director CPU · G4 deriva 12 cuadros · (G5 audio se mueve a D2). Cada gate = script + commit con cifras.
74
+ - **F2 · Pipeline core (D2):** Director GBNF + SceneState + cut/continuity · ASR+VAD · Recital · depth · UI provisional Blocks end-to-end.
75
+ - **F3 · Sala de cine (D3):** frontend `gr.Server` completo (paralaje WebGL, transiciones, grano, claqueta, subtítulos) · audio ambiental · lanzar LoRA en Modal (overnight).
76
+ - **F4 · Artefactos (D4):** Replay theater · export MP4 (ffmpeg+RIFE) · storyboard · Showcase · LoRA integrada · publicar LoRA/trazas/blog · README · **freeze 20:00**.
77
+ - **F5 · Submission (D5):** video, post social, enlaces, ensayo de juez, envío antes del mediodía.
78
+ - **F6 · Contingencia:** orden de corte pre-acordado: 1º RIFE → 2º audio → 3º Aya → 4º LoRA. Jamás: continuidad, paralaje, claqueta, recital, showcase.
79
+
80
+ ## 6. Principios de decisión durante la construcción
81
+
82
+ 1. Cumplimiento de reglas > probabilidad de ganar > demo > diferenciación > elegancia.
83
+ 2. Todo lo no verificado lleva etiqueta `VERIFY:` en el código/doc y se resuelve antes de depender de ello.
84
+ 3. Cada afirmación de la submission (insignias, "local", params) debe ser demostrable con un artefacto del repo.
85
+ 4. Si una pieza amenaza el Must-set, se corta según F6 sin debate.
README.md CHANGED
@@ -1,13 +1,15 @@
1
  ---
2
- title: Lightloom Dev
3
- emoji: 🐢
4
- colorFrom: purple
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.17.3
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Lightloom · live narrated cinema
3
+ emoji: 🎞️
 
 
4
  sdk: gradio
 
 
5
  app_file: app.py
 
6
  ---
7
 
8
+ # Lightloom
9
+
10
+ Private development build for the Hugging Face Build Small hackathon.
11
+
12
+ Demo video: ⟦PLACEHOLDER⟧ · Social post: ⟦PLACEHOLDER⟧ · Code: ⟦PLACEHOLDER⟧
13
+
14
+ The README will be completed from `docs/08_COMPLIANCE_HACKATHON.md` during WP-13. Placeholders are
15
+ intentional while privacy mode is active.
app.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Lightloom Gradio Server entrypoint.
2
+
3
+ Parameter contribution: 0B. Runtime endpoints expose app health and private
4
+ benchmark hooks; model loading stays inside explicit benchmark/pipeline calls.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import gradio as gr
14
+ from fastapi.responses import HTMLResponse
15
+
16
+ from lightloom.compliance.params_ledger import entries, total_runtime_params
17
+ from lightloom.core.config import CONFIG, LIGHTLOOM_PROFILE
18
+
19
+ app = gr.Server(title="Lightloom", version="0.1.0")
20
+
21
+
22
+ @app.get("/", response_class=HTMLResponse)
23
+ def index() -> str:
24
+ return """
25
+ <!doctype html>
26
+ <html lang="en">
27
+ <head>
28
+ <meta charset="utf-8" />
29
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
30
+ <title>Lightloom</title>
31
+ <style>
32
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center;
33
+ background: #0A0A0C; color: #F5EFE6; font-family: Inter, system-ui, sans-serif; }
34
+ main { max-width: 720px; padding: 32px; }
35
+ h1 { font-family: Georgia, serif; font-size: clamp(42px, 8vw, 92px); margin: 0 0 12px; }
36
+ a { color: #E8A33D; }
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <main>
41
+ <h1>Lightloom</h1>
42
+ <p>Private build. The projector is being assembled.</p>
43
+ <p><a href="/health">health</a></p>
44
+ </main>
45
+ </body>
46
+ </html>
47
+ """
48
+
49
+
50
+ @app.get("/health")
51
+ def health() -> dict[str, Any]:
52
+ return {
53
+ "app": "lightloom",
54
+ "profile": LIGHTLOOM_PROFILE,
55
+ "config": {
56
+ "width": CONFIG.width,
57
+ "height": CONFIG.height,
58
+ "flux_dtype": CONFIG.flux_dtype,
59
+ "flux_aot": CONFIG.flux_aot,
60
+ "showcase_only": CONFIG.showcase_only,
61
+ },
62
+ "params_total": total_runtime_params(),
63
+ "params_limit": 32_000_000_000,
64
+ "ledger": [entry.__dict__ for entry in entries()],
65
+ "privacy_mode": os.getenv("LIGHTLOOM_PRIVACY_MODE", "1") == "1",
66
+ }
67
+
68
+
69
+ @app.post("/internal/bench/g1")
70
+ def run_g1_endpoint() -> dict[str, Any]:
71
+ if os.getenv("LIGHTLOOM_ENABLE_INTERNAL_BENCH", "0") != "1":
72
+ return {"ok": False, "error": "internal benchmark endpoint disabled"}
73
+ from benchmarks.gate_g1 import run
74
+
75
+ data = run(dry_run=False, reps=int(os.getenv("LIGHTLOOM_G1_REPS", "5")), allow_local=False)
76
+ return {
77
+ "ok": True,
78
+ "data": data,
79
+ "decision": data["decision"],
80
+ "result_path": str(Path("benchmarks/results/g1.json")),
81
+ }
82
+
83
+
84
+ if __name__ == "__main__":
85
+ app.launch()
benchmarks/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Benchmark scripts for Lightloom gates."""
benchmarks/gate_g1.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gate G1: FLUX.2 Klein latency benchmark.
2
+
3
+ Parameter contribution: 0B. This script loads runtime model weights only when
4
+ executed in benchmark mode. Local dry-runs are non-authoritative and must never
5
+ be used as Space benchmark numbers.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ from pathlib import Path
13
+ import statistics
14
+ import sys
15
+ import time
16
+ from typing import Any
17
+
18
+ ROOT = Path(__file__).resolve().parents[1]
19
+ sys.path.insert(0, str(ROOT / "src"))
20
+
21
+ from lightloom.core.config import MODEL_REFS # noqa: E402
22
+
23
+ RESULTS_DIR = ROOT / "benchmarks" / "results"
24
+ DEFAULT_JSON = RESULTS_DIR / "g1.json"
25
+ DEFAULT_MD = RESULTS_DIR / "g1.md"
26
+ PROMPT = (
27
+ "cinematic illustrated film still, an old lighthouse at the edge of the world, "
28
+ "stormy dusk, warm lantern glow, painterly texture, volumetric light, 2.39:1 frame"
29
+ )
30
+ RESOLUTIONS = ((768, 432), (1024, 576))
31
+ DTYPES = ("fp8", "bf16")
32
+ AOT_FLAGS = (False, True)
33
+
34
+
35
+ def _percentile(values: list[float], q: float) -> float:
36
+ if not values:
37
+ return 0.0
38
+ ordered = sorted(values)
39
+ idx = min(len(ordered) - 1, max(0, round((len(ordered) - 1) * q)))
40
+ return ordered[idx]
41
+
42
+
43
+ def _torch_dtype(name: str) -> Any:
44
+ import torch
45
+
46
+ if name == "bf16":
47
+ return torch.bfloat16
48
+ if name == "fp8":
49
+ return torch.float8_e4m3fn
50
+ raise ValueError(f"unsupported dtype: {name}")
51
+
52
+
53
+ def _hardware_profile() -> dict[str, Any]:
54
+ import os
55
+ import platform
56
+
57
+ profile: dict[str, Any] = {
58
+ "lightloom_profile": os.getenv("LIGHTLOOM_PROFILE", "local"),
59
+ "space_id": os.getenv("SPACE_ID") or os.getenv("LIGHTLOOM_DEV_SPACE_ID"),
60
+ "platform": platform.platform(),
61
+ }
62
+ try:
63
+ import torch
64
+
65
+ profile.update(
66
+ {
67
+ "torch": torch.__version__,
68
+ "cuda": getattr(torch.version, "cuda", None),
69
+ "cuda_available": bool(torch.cuda.is_available()),
70
+ "device": torch.cuda.get_device_name(0) if torch.cuda.is_available() else None,
71
+ "arch_list": list(torch.cuda.get_arch_list()) if torch.cuda.is_available() else [],
72
+ }
73
+ )
74
+ except Exception as exc: # noqa: BLE001
75
+ profile["torch_error"] = f"{type(exc).__name__}: {exc}"
76
+ return profile
77
+
78
+
79
+ def _dry_run_combination(width: int, height: int, dtype: str, aot: bool, reps: int) -> dict[str, Any]:
80
+ timings = []
81
+ for i in range(reps):
82
+ start = time.perf_counter()
83
+ time.sleep(0.002 + (i * 0.0001))
84
+ timings.append((time.perf_counter() - start) * 1000)
85
+ return {
86
+ "width": width,
87
+ "height": height,
88
+ "dtype": dtype,
89
+ "aot": aot,
90
+ "status": "dry_run",
91
+ "authoritative": False,
92
+ "timings_ms": [round(v, 3) for v in timings],
93
+ "p50_ms": round(statistics.median(timings), 3),
94
+ "p95_ms": round(_percentile(timings, 0.95), 3),
95
+ "note": "plumbing validation only; not a benchmark",
96
+ }
97
+
98
+
99
+ def _load_pipeline(dtype: str) -> Any:
100
+ import torch
101
+ from diffusers import Flux2KleinPipeline
102
+
103
+ ref = MODEL_REFS["painter"]
104
+ return Flux2KleinPipeline.from_pretrained(
105
+ ref.repo_id,
106
+ revision=ref.revision,
107
+ torch_dtype=_torch_dtype(dtype),
108
+ ).to("cuda")
109
+
110
+
111
+ def _maybe_compile(pipe: Any) -> str:
112
+ import torch
113
+
114
+ if not hasattr(pipe, "transformer"):
115
+ return "no_transformer_attr"
116
+ pipe.transformer = torch.compile(pipe.transformer, mode="reduce-overhead", fullgraph=False)
117
+ return "torch.compile(reduce-overhead)"
118
+
119
+
120
+ def _benchmark_combination(
121
+ pipe: Any,
122
+ width: int,
123
+ height: int,
124
+ dtype: str,
125
+ aot: bool,
126
+ reps: int,
127
+ ) -> dict[str, Any]:
128
+ import torch
129
+
130
+ compile_mode = "off"
131
+ if aot:
132
+ compile_mode = _maybe_compile(pipe)
133
+ generator = torch.Generator(device="cuda").manual_seed(1901)
134
+ timings: list[float] = []
135
+ try:
136
+ with torch.inference_mode():
137
+ pipe(
138
+ prompt=PROMPT,
139
+ height=height,
140
+ width=width,
141
+ num_inference_steps=4,
142
+ guidance_scale=1.0,
143
+ generator=generator,
144
+ output_type="pil",
145
+ )
146
+ torch.cuda.synchronize()
147
+ for rep in range(reps):
148
+ generator = torch.Generator(device="cuda").manual_seed(1901 + rep)
149
+ start = time.perf_counter()
150
+ pipe(
151
+ prompt=PROMPT,
152
+ height=height,
153
+ width=width,
154
+ num_inference_steps=4,
155
+ guidance_scale=1.0,
156
+ generator=generator,
157
+ output_type="pil",
158
+ )
159
+ torch.cuda.synchronize()
160
+ timings.append((time.perf_counter() - start) * 1000)
161
+ return {
162
+ "width": width,
163
+ "height": height,
164
+ "dtype": dtype,
165
+ "aot": aot,
166
+ "status": "ok",
167
+ "authoritative": True,
168
+ "compile_mode": compile_mode,
169
+ "timings_ms": [round(v, 3) for v in timings],
170
+ "p50_ms": round(statistics.median(timings), 3),
171
+ "p95_ms": round(_percentile(timings, 0.95), 3),
172
+ }
173
+ except Exception as exc: # noqa: BLE001
174
+ return {
175
+ "width": width,
176
+ "height": height,
177
+ "dtype": dtype,
178
+ "aot": aot,
179
+ "status": "failed",
180
+ "authoritative": True,
181
+ "compile_mode": compile_mode,
182
+ "error": f"{type(exc).__name__}: {exc}",
183
+ }
184
+
185
+
186
+ def _decide(results: list[dict[str, Any]]) -> dict[str, Any]:
187
+ successful = [item for item in results if item.get("status") == "ok" and item.get("authoritative")]
188
+ if not successful:
189
+ return {"status": "blocked", "reason": "no authoritative successful G1 run"}
190
+ by_resolution = {
191
+ (item["width"], item["height"]): item
192
+ for item in sorted(successful, key=lambda x: x["p50_ms"])
193
+ if item["dtype"] == "fp8"
194
+ }
195
+ high = by_resolution.get((1024, 576))
196
+ low = by_resolution.get((768, 432))
197
+ if high and high["p50_ms"] <= 1800:
198
+ return {"status": "pass", "resolution": [1024, 576], "selected": high}
199
+ if low and low["p50_ms"] <= 3000:
200
+ return {"status": "fallback", "resolution": [768, 432], "selected": low}
201
+ return {"status": "fallback_polaroid", "resolution": [768, 432], "selected": low or high}
202
+
203
+
204
+ def _write_markdown(path: Path, data: dict[str, Any]) -> None:
205
+ rows = [
206
+ "# Gate G1 Results",
207
+ "",
208
+ f"Authoritative: `{data['authoritative']}`",
209
+ f"Hardware profile: `{data['hardware_profile'].get('lightloom_profile')}`",
210
+ "",
211
+ "| Resolution | dtype | AoT | status | p50 ms | p95 ms |",
212
+ "|---|---|---:|---|---:|---:|",
213
+ ]
214
+ for item in data["results"]:
215
+ rows.append(
216
+ "| {w}x{h} | {dtype} | {aot} | {status} | {p50} | {p95} |".format(
217
+ w=item["width"],
218
+ h=item["height"],
219
+ dtype=item["dtype"],
220
+ aot="yes" if item["aot"] else "no",
221
+ status=item["status"],
222
+ p50=item.get("p50_ms", ""),
223
+ p95=item.get("p95_ms", ""),
224
+ )
225
+ )
226
+ rows.extend(["", f"Decision: `{data['decision']['status']}`"])
227
+ path.write_text("\n".join(rows) + "\n", encoding="utf-8")
228
+
229
+
230
+ def run(dry_run: bool, reps: int, allow_local: bool) -> dict[str, Any]:
231
+ import os
232
+
233
+ profile = os.getenv("LIGHTLOOM_PROFILE", "local")
234
+ if not dry_run and profile != "space" and not allow_local:
235
+ raise SystemExit(
236
+ "Refusing to run G1 benchmark outside LIGHTLOOM_PROFILE=space. "
237
+ "Use --dry-run for plumbing or --allow-local for private debugging; "
238
+ "local timings are non-authoritative."
239
+ )
240
+ RESULTS_DIR.mkdir(parents=True, exist_ok=True)
241
+ results: list[dict[str, Any]] = []
242
+ if dry_run:
243
+ for width, height in RESOLUTIONS:
244
+ for dtype in DTYPES:
245
+ for aot in AOT_FLAGS:
246
+ results.append(_dry_run_combination(width, height, dtype, aot, reps))
247
+ else:
248
+ for dtype in DTYPES:
249
+ try:
250
+ pipe = _load_pipeline(dtype)
251
+ except Exception as exc: # noqa: BLE001
252
+ for width, height in RESOLUTIONS:
253
+ for aot in AOT_FLAGS:
254
+ results.append(
255
+ {
256
+ "width": width,
257
+ "height": height,
258
+ "dtype": dtype,
259
+ "aot": aot,
260
+ "status": "failed",
261
+ "authoritative": profile == "space",
262
+ "error": f"load failed: {type(exc).__name__}: {exc}",
263
+ }
264
+ )
265
+ continue
266
+ for width, height in RESOLUTIONS:
267
+ for aot in AOT_FLAGS:
268
+ results.append(_benchmark_combination(pipe, width, height, dtype, aot, reps))
269
+ del pipe
270
+ data = {
271
+ "schema_version": 1,
272
+ "gate": "G1",
273
+ "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
274
+ "authoritative": (not dry_run and profile == "space"),
275
+ "hardware_profile": _hardware_profile(),
276
+ "reps": reps,
277
+ "prompt": PROMPT,
278
+ "results": results,
279
+ "decision": _decide(results),
280
+ }
281
+ DEFAULT_JSON.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
282
+ _write_markdown(DEFAULT_MD, data)
283
+ return data
284
+
285
+
286
+ def main() -> int:
287
+ parser = argparse.ArgumentParser()
288
+ parser.add_argument("--dry-run", action="store_true")
289
+ parser.add_argument("--allow-local", action="store_true")
290
+ parser.add_argument("--reps", type=int, default=5)
291
+ args = parser.parse_args()
292
+ data = run(dry_run=args.dry_run, reps=args.reps, allow_local=args.allow_local)
293
+ print(f"G1 wrote {DEFAULT_JSON}")
294
+ print(f"G1 decision: {data['decision']['status']}")
295
+ if args.dry_run:
296
+ return 0
297
+ return 0 if data["decision"]["status"] in {"pass", "fallback", "fallback_polaroid"} else 1
298
+
299
+
300
+ if __name__ == "__main__":
301
+ raise SystemExit(main())
benchmarks/results/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
benchmarks/results/preflight.json ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "all_green": true,
3
+ "checks": [
4
+ {
5
+ "evidence": {
6
+ "org_present": true,
7
+ "required_org": "build-small-hackathon",
8
+ "user": "Efradeca"
9
+ },
10
+ "message": "authenticated user is a member of the hackathon org",
11
+ "name": "P1 org membership",
12
+ "ok": true
13
+ },
14
+ {
15
+ "evidence": {
16
+ "created_or_reused": true,
17
+ "hardware": "None",
18
+ "privacy": "private",
19
+ "requested_hardware": "zero-a10g",
20
+ "sdk": "None",
21
+ "space_id": "Efradeca/lightloom-dev",
22
+ "stage": "NO_APP_FILE"
23
+ },
24
+ "message": "private dev Space exists and appears to be ZeroGPU/Gradio",
25
+ "name": "P2 private ZeroGPU Space",
26
+ "ok": true
27
+ },
28
+ {
29
+ "evidence": {
30
+ "secret_names_only": [
31
+ "HF_TOKEN"
32
+ ]
33
+ },
34
+ "message": "read token env var is available for configuring the Space secret",
35
+ "name": "P3 runtime secret contract",
36
+ "ok": true
37
+ },
38
+ {
39
+ "evidence": {
40
+ "executable": "C:\\Users\\dynam\\Documents\\CodingProjects\\Lightloom\\.venv\\Scripts\\python.exe",
41
+ "imports": {
42
+ "diffusers": {
43
+ "detail": "0.39.0.dev0",
44
+ "ok": true
45
+ },
46
+ "gradio": {
47
+ "detail": "6.17.3",
48
+ "ok": true
49
+ },
50
+ "huggingface_hub": {
51
+ "detail": "1.18.0",
52
+ "ok": true
53
+ },
54
+ "llama_cpp": {
55
+ "detail": "0.3.28",
56
+ "ok": true
57
+ },
58
+ "torch": {
59
+ "detail": "2.11.0+cu128",
60
+ "ok": true
61
+ },
62
+ "transformers": {
63
+ "detail": "5.11.0",
64
+ "ok": true
65
+ }
66
+ },
67
+ "packages_txt_has_ffmpeg": true,
68
+ "platform": "Windows-11-10.0.26200-SP0",
69
+ "python": "3.12.10 (tags/v3.12.10:0cc8128, Apr 8 2025, 12:21:36) [MSC v.1943 64 bit (AMD64)]",
70
+ "torch": {
71
+ "arch_list": [
72
+ "sm_75",
73
+ "sm_80",
74
+ "sm_86",
75
+ "sm_90",
76
+ "sm_100",
77
+ "sm_120"
78
+ ],
79
+ "cuda_available": true,
80
+ "cuda_kernel_smoke": true,
81
+ "cuda_version": "12.8",
82
+ "sm_120_supported": true,
83
+ "torch": "2.11.0+cu128"
84
+ }
85
+ },
86
+ "message": "Python, required imports, and local CUDA smoke check are available",
87
+ "name": "P4 local versions",
88
+ "ok": true
89
+ },
90
+ {
91
+ "evidence": {
92
+ "failures": {},
93
+ "local_aot": false,
94
+ "local_flux_dtype": "fp8",
95
+ "local_resolution": [
96
+ 768,
97
+ 432
98
+ ],
99
+ "profile": "local",
100
+ "resolved": {
101
+ "asr": {
102
+ "license": "apache-2.0",
103
+ "private": false,
104
+ "repo_id": "CohereLabs/cohere-transcribe-03-2026",
105
+ "sha": "b1eacc2686a3d08ceaae5f24a88b1d519620bc09"
106
+ },
107
+ "depth": {
108
+ "license": "apache-2.0",
109
+ "private": false,
110
+ "repo_id": "depth-anything/Depth-Anything-V2-Small-hf",
111
+ "sha": "5426e4f0f36572d16453bbda7a8389317b1bef99"
112
+ },
113
+ "director": {
114
+ "license": "apache-2.0",
115
+ "private": false,
116
+ "repo_id": "openbmb/MiniCPM5-1B",
117
+ "sha": "4e9de7a0778dc1c362e983e6858f0e77542cbdca"
118
+ },
119
+ "painter": {
120
+ "license": "apache-2.0",
121
+ "private": false,
122
+ "repo_id": "black-forest-labs/FLUX.2-klein-4B",
123
+ "sha": "e7b7dc27f91deacad38e78976d1f2b499d76a294"
124
+ },
125
+ "painter_base_train": {
126
+ "license": "apache-2.0",
127
+ "private": false,
128
+ "repo_id": "black-forest-labs/FLUX.2-klein-base-4B",
129
+ "sha": "a3b4f4849157f664bdbc776fd7453c2783562f4d"
130
+ },
131
+ "rife": {
132
+ "license": "VERIFY",
133
+ "note": "GitHub repo; checkpoint license remains VERIFY until selected.",
134
+ "repo_id": "hzwer/Practical-RIFE"
135
+ },
136
+ "sound": {
137
+ "license": "other",
138
+ "private": false,
139
+ "repo_id": "stabilityai/stable-audio-open-small",
140
+ "sha": "dc620d91535857b72ebb59b4ca45978db6d417f5"
141
+ },
142
+ "translator": {
143
+ "license": "cc-by-nc-4.0",
144
+ "private": false,
145
+ "repo_id": "CohereLabs/tiny-aya-global-GGUF",
146
+ "sha": "a602ea7eeec3a4ad6f77a1b8cf6a53512824922b"
147
+ },
148
+ "vad": {
149
+ "license": "mit",
150
+ "private": false,
151
+ "repo_id": "onnx-community/silero-vad",
152
+ "sha": "e71cae966052b992a7eca6b17738916ce0eca4ec"
153
+ }
154
+ }
155
+ },
156
+ "message": "model cards resolved",
157
+ "name": "P5 model cards",
158
+ "ok": true
159
+ },
160
+ {
161
+ "evidence": {
162
+ "estimated_runtime_weights_gb": 20,
163
+ "free_gb": 317.27
164
+ },
165
+ "message": "local disk has room for iterative caches",
166
+ "name": "P6 disk/cache",
167
+ "ok": true
168
+ },
169
+ {
170
+ "evidence": {
171
+ "detail": "installed"
172
+ },
173
+ "message": "spaces package imports",
174
+ "name": "P7 spaces decorator",
175
+ "ok": true
176
+ },
177
+ {
178
+ "evidence": {
179
+ "match_files": []
180
+ },
181
+ "message": "no HF token patterns in tracked files",
182
+ "name": "secret scan",
183
+ "ok": true
184
+ }
185
+ ],
186
+ "elapsed_s": 7.365,
187
+ "generated_at": "2026-06-11T19:16:39Z",
188
+ "hardware_profile": "local",
189
+ "privacy_mode": true,
190
+ "schema_version": 1
191
+ }
docs/01_ARCHITECTURE.md ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 01 · ARQUITECTURA — Lightloom
2
+
3
+ ## 1. Stack
4
+
5
+ | Capa | Tecnología | Nota |
6
+ |---|---|---|
7
+ | Hosting | HF Space (org `build-small-hackathon`), SDK Gradio, hardware ZeroGPU (slice H200, 70GB) | Regla R2 |
8
+ | Backend | `gradio.Server` (FastAPI extendido: colas, SSE streaming, ZeroGPU, compat. gradio_client) | Patrón validado por blog oficial de Gradio |
9
+ | Inferencia GPU | PyTorch + diffusers (git) + transformers ≥5.4 + stable-audio-tools, dentro de funciones `@spaces.GPU` | FP8 + AoT compile (blog zerogpu-aoti) |
10
+ | Inferencia CPU | llama.cpp (`llama-cpp-python`, wheel CPU) para Director (gate G3) y Aya (gate G2); Silero VAD | GBNF grammar |
11
+ | Frontend | HTML/CSS/JS vanilla + WebGL2 (shaders propios: paralaje, grano; transiciones estilo gl-transitions) servido por `gr.Server` en `/` | Sin framework → cero build step, carga instantánea |
12
+ | Export | ffmpeg (vía `packages.txt`) + RIFE (PyTorch) | Solo en export, no en vivo |
13
+ | Entrenamiento (build-time) | Modal: LoRA sobre FLUX.2 klein **base** 4B → publicar en Hub | No es runtime |
14
+
15
+ ## 2. Estructura de carpetas (crear tal cual)
16
+
17
+ ```
18
+ lightloom/
19
+ ├── AGENTS.md · CLAUDE.md · MASTER_PLAN.md · README.md (template en docs/08)
20
+ ├── app.py # entrypoint gr.Server: rutas, api endpoints, estáticos
21
+ ├── requirements.txt · packages.txt (ffmpeg) · .gitignore
22
+ ├── src/lightloom/
23
+ │ ├── core/
24
+ │ │ ├── config.py # ids+revisions de modelos, knobs, flags (SHOWCASE_ONLY, etc.)
25
+ │ │ ├── session.py # estado por sesión: Film (beats, frames, audio refs)
26
+ │ │ ├── beats.py # segmentación VAD → Beat
27
+ │ │ └── budget.py # contador de segundos GPU por sesión + quota guard
28
+ │ ├── audio_in/ (vad.py, asr.py)
29
+ │ ├── translate/ (aya.py) # condicional gate G2
30
+ │ ├── director/ (state.py, director.py, grammar.gbnf, schema.py)
31
+ │ ├── paint/ (klein.py, lora.py, anchors.py)
32
+ │ ├── depth/ (depth.py)
33
+ │ ├── sound/ (ambient.py) # condicional gate G5
34
+ │ ├── export/ (rife.py, ffmpeg_mux.py, storyboard.py)
35
+ │ └── compliance/(params_ledger.py, offgrid_check.py)
36
+ ├── frontend/
37
+ │ ├── index.html
38
+ │ ├── css/ (tokens.css, stage.css)
39
+ │ └── js/ (api.js, recorder.js, stage.js, parallax.js, transitions.js,
40
+ │ slate.js, subtitles.js, audio_mix.js, theater.js, showcase.js)
41
+ ├── benchmarks/ (gate_g1..g5.py, results/) # resultados solo por ejecución
42
+ ├── tests/ (test_grammar.py, test_state.py, test_beats.py, test_compliance.py,
43
+ │ test_ledger.py, golden/poema_faro.txt)
44
+ ├── training/modal_lora/ (launch.py, train.py, captions/, publish.py)
45
+ ├── assets/ (anchors/ fonts/(OFL only) showcase/ ui/)
46
+ ├── scripts/ (preflight.py, build_space.sh, publish_traces.py, release_check.py)
47
+ └── docs/ (este paquete + VERIFY_LOG.md)
48
+ ```
49
+
50
+ ## 3. Contratos de datos (la columna vertebral — escribir tests primero)
51
+
52
+ ```python
53
+ # Beat — unidad de narración detectada por VAD
54
+ Beat = {id:int, t_start:float, t_end:float, audio_path:str, lang:str,
55
+ text_raw:str, text_en:str}
56
+
57
+ # SceneState — memoria del Director (persistente entre beats)
58
+ SceneState = {scene_id:int, characters:[str], setting:str, era:str,
59
+ lighting:str, palette:[hex,hex,hex], mood:str, motifs:[str]}
60
+
61
+ # Shot — salida del Director por beat (validada por GBNF + pydantic)
62
+ Shot = {decision:"cut"|"continuity", shot_scale:"ECU"|"CU"|"MS"|"WS"|"EWS",
63
+ camera_move:"static"|"dolly_in"|"dolly_out"|"pan_left"|"pan_right"|"tilt_up"|"crane_down",
64
+ lighting:str, palette:[hex,hex,hex],
65
+ image_prompt_en:str, ambient_prompt_en:str,
66
+ transition:"hard_cut"|"crossfade"|"wipe_left"|"iris"}
67
+
68
+ # Frame — resultado del Pintor + Profundidad
69
+ Frame = {beat_id:int, image_path:str, depth_path:str, seed:int, refs_used:[str],
70
+ gen_ms:int, audio_bed_path:str|None}
71
+
72
+ # Film — sesión completa (alimenta Replay, Export y Trazas)
73
+ Film = {session_id:str, lang:str, beats:[Beat], shots:[Shot], frames:[Frame],
74
+ narration_audio_path:str|None, created_at:iso}
75
+ ```
76
+
77
+ Las **trazas** publicadas (Sharing is Caring) son `Film` serializado menos audio del usuario
78
+ (privacidad): solo nuestros demos consentidos van con audio.
79
+
80
+ ## 4. Estrategia ZeroGPU (crítica — leer antes de tocar `paint/`)
81
+
82
+ 1. **Carga:** pipelines se instancian en el proceso principal en CPU al arrancar; dentro de cada
83
+ función `@spaces.GPU(duration=...)` se mueven/usan en CUDA. Mantener UNA función GPU por beat
84
+ que encadene ASR→imagen→depth→audio para pagar un solo ciclo de asignación.
85
+ 2. **Optimización:** checkpoint FP8 de klein + compilación ahead-of-time según el blog oficial
86
+ `zerogpu-aoti` (1.3–1.8× en modelos clase Flux). El artefacto AoT se construye una vez
87
+ (script `paint/aot_build.py`) y se cachea.
88
+ 3. **Presupuesto:** `core/budget.py` contabiliza segundos GPU por sesión; al superar
89
+ `SESSION_GPU_BUDGET` (config, ~90 s) la UI ofrece Replay/Export y sugiere Showcase. Nunca
90
+ cortar a mitad de beat.
91
+ 4. **Quota guard:** si la asignación ZeroGPU falla (cuota global), el backend emite estado
92
+ `quota_exceeded` → frontend muestra S3 Showcase con honestidad ("la sala está llena…").
93
+ 5. **Concurrencia:** `concurrency_limit=1` por sesión en endpoints GPU; cola Gradio visible
94
+ como estado "esperando proyector" en UI.
95
+
96
+ ## 5. Endpoints `gr.Server` (app.py)
97
+
98
+ | Ruta | Tipo | Función |
99
+ |---|---|---|
100
+ | `GET /` | estático | `frontend/index.html` (la Sala) |
101
+ | `@app.api("beat")` | generador (SSE) | entrada: audio chunk o texto + lang + session_id → emite eventos `{stage: transcribed|translated|directed|painted|depth|sound, payload}` por etapa. `# VERIFY:` confirmar API exacta de streaming por generador en la guía server-mode al implementar |
102
+ | `@app.api("recital")` | generador | entrada: poema completo + lang → itera estrofas como beats |
103
+ | `@app.api("export")` | función | session_id → MP4 (RIFE+xfade+mezcla) y storyboard PNG; devuelve URLs de descarga |
104
+ | `@app.api("film")` | función | session_id → JSON `Film` (para Replay y para depurar) |
105
+ | `GET /showcase/*` | estático | películas pre-renderizadas etiquetadas |
106
+ | `GET /health` | función | params ledger + flags + versión (transparencia para jueces) |
107
+
108
+ ## 6. Diagramas (mermaid)
109
+
110
+ ### 6.1 Secuencia de un beat
111
+ ```mermaid
112
+ sequenceDiagram
113
+ participant U as Usuario (mic)
114
+ participant FE as Frontend (Sala)
115
+ participant BE as gr.Server
116
+ participant GPU as @spaces.GPU
117
+ participant DIR as Director (llama.cpp CPU)
118
+ U->>FE: habla… (MediaRecorder)
119
+ FE->>FE: VAD detecta pausa → beat
120
+ FE->>BE: POST beat(audio, lang)
121
+ BE->>GPU: transcribe(audio, lang) %% Cohere Transcribe
122
+ GPU-->>BE: text_raw
123
+ BE-->>FE: SSE stage=transcribed (subtítulo)
124
+ alt lang != EN y Aya activo (G2)
125
+ BE->>DIR: aya.translate(text_raw)
126
+ DIR-->>BE: text_en
127
+ end
128
+ BE->>DIR: director(SceneState, text_en) + GBNF
129
+ DIR-->>BE: Shot JSON (cut|continuity, cámara, prompts)
130
+ BE-->>FE: SSE stage=directed (claqueta)
131
+ BE->>GPU: klein(prompt, refs=policy(Shot)) + depth + ambient
132
+ GPU-->>BE: frame.png, depth.png, bed.wav
133
+ BE-->>FE: SSE stage=painted (transición + paralaje + soundbed)
134
+ ```
135
+
136
+ ### 6.2 Máquina de estados de la Sala (frontend)
137
+ ```mermaid
138
+ stateDiagram-v2
139
+ [*] --> Lobby
140
+ Lobby --> Listening: REC / Recital
141
+ Listening --> Processing: beat detectado
142
+ Processing --> Revealing: frame listo
143
+ Revealing --> Listening: sigue narrando
144
+ Listening --> Theater: STOP
145
+ Processing --> ErrorSoft: fallo etapa
146
+ ErrorSoft --> Listening: reintento/skip beat
147
+ Lobby --> Showcase: cuota agotada / explorar
148
+ Theater --> Export
149
+ Theater --> Lobby
150
+ ```
151
+
152
+ ### 6.3 Despliegue
153
+ ```mermaid
154
+ flowchart LR
155
+ subgraph Space[HF Space · ZeroGPU]
156
+ FE[frontend estático] --- GS[gr.Server]
157
+ GS --> CPU[llama.cpp: Director(+Aya) · VAD]
158
+ GS --> G[@spaces.GPU: Transcribe · klein FP8/AoT · DepthV2-S · StableAudio · RIFE(export)]
159
+ GS --> FF[ffmpeg]
160
+ end
161
+ Hub[(HF Hub: pesos · LoRA · trazas · showcase)] --> Space
162
+ Modal[Modal · build-time LoRA] -.publica.-> Hub
163
+ GH[(GitHub público · commits Codex)] -.referencia.-> Space
164
+ ```
165
+
166
+ ## 7. Decisiones de arquitectura registradas (ADR breves)
167
+
168
+ - **ADR-1 · Un solo `@spaces.GPU` por beat** (no por modelo): minimiza overhead de asignación;
169
+ trade-off: función más larga → fijar `duration` con margen medido en G1.
170
+ - **ADR-2 · Director en CPU por defecto** (gate G3): llama.cpp-CUDA bajo ZeroGPU no está
171
+ garantizado (docs: compatibilidad orientada a PyTorch). Fallback transformers-GPU preparado.
172
+ - **ADR-3 · Frontend sin framework:** elimina riesgo de build en el Space y maximiza control de
173
+ shaders; el costo (más JS a mano) se paga con módulos pequeños y spec detallada (docs/04).
174
+ - **ADR-4 · RIFE solo en export:** evita ghosting en vivo y mantiene el presupuesto GPU del beat.
175
+ - **ADR-5 · Showcase como ciudadano de primera:** la regla del evento asume fallos de GPU en
176
+ evaluación; diseñarlo desde el día 1, no como parche.
docs/02_MODELS_AND_MATH.md ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 02 · MODELOS Y MATEMÁTICA — Lightloom
2
+
3
+ ## 1. Registro de modelos (IDs candidatos → confirmar en P5 del preflight y fijar revision)
4
+
5
+ | Rol | Repo id (candidato) | Estado | Licencia (copiar literal de la card) |
6
+ |---|---|---|---|
7
+ | ASR | `CohereLabs/cohere-transcribe-03-2026` | Verificado por preflight P5 | apache-2.0 |
8
+ | Traductor | `CohereLabs/tiny-aya-global-GGUF` (Q4_K_M) | Verificado por preflight P5 | cc-by-nc-4.0 |
9
+ | Director | `openbmb/MiniCPM5-1B` (+ GGUF: `VERIFY:` id exacto del repo GGUF en HF; existe vía Ollama/ModelScope) | Card BF16 verificada por preflight P5 | apache-2.0 |
10
+ | Pintor (runtime) | `black-forest-labs/FLUX.2-klein-4B` (+ checkpoint FP8 oficial) | Verificado por preflight P5 | apache-2.0 |
11
+ | Pintor (entreno) | `black-forest-labs/FLUX.2-klein-base-4B` | Verificado por preflight P5 | apache-2.0 |
12
+ | Profundidad | `depth-anything/Depth-Anything-V2-Small-hf` | Verificado por preflight P5 | apache-2.0 |
13
+ | Sonidista | `stabilityai/stable-audio-open-small` | Verificado por preflight P5 | other (card text: Stability AI Community License) |
14
+ | Interpolador | RIFE 4.x lite (repo `hzwer/Practical-RIFE`) | Verificado (paper+repo) | `VERIFY:` licencia del checkpoint elegido |
15
+ | VAD | `onnx-community/silero-vad` | Verificado por preflight P5 | mit |
16
+
17
+ Regla: ningún modelo se importa en runtime sin entrada en `compliance/params_ledger.py`
18
+ (`register(name, params, runtime=True/False, license=...)`). El README publica la tabla generada.
19
+
20
+ ## 2. Matemática por componente (lo que el código implementa — sin inventos)
21
+
22
+ ### 2.1 Segmentación en beats (VAD con histéresis)
23
+ Silero VAD da probabilidad de voz p_t por ventana (~30 ms). Definimos estado hablando/silencio con
24
+ histéresis: entra en habla si p_t > θ_on (0.6), sale si p_t < θ_off (0.35) sostenido τ_sil ≥ 600 ms.
25
+ Un **beat** se cierra al salir de habla si su duración ∈ [T_min=1.2 s, T_max=12 s]; si supera T_max
26
+ se corta en la frontera de silencio más cercana. Justificación: beats ≈ frases/alientos del
27
+ narrador → 1 plano por aliento, el ritmo natural del cine narrado.
28
+
29
+ ### 2.2 Generación klein (rectified flow, destilado a 4 pasos)
30
+ klein es un rectified-flow transformer: define trayectoria lineal x_t = (1−t)·x₀ + t·ε y la red
31
+ predice velocidad v_θ(x_t, t, c). Muestreo = integrar dx/dt = v_θ con Euler en una malla de
32
+ **4 pasos** (versión distilled, guidance_scale=1.0 — valores de la model card oficial). Con FP8 +
33
+ compilación AoT (blog zerogpu-aoti: 1.3–1.8× en clase Flux) el objetivo es t_img ≤ 1.8 s a
34
+ 1024×576 (16:9 dentro del letterbox 2.39:1; múltiplos de 16 obligatorios — restricción de klein).
35
+
36
+ ### 2.3 Continuidad multi-referencia con caché KV
37
+ El pipeline Flux2 Klein KV (docs de diffusers) incorpora tokens de imágenes de referencia en el
38
+ primer paso de denoising y **cachea sus proyecciones K/V** para reutilizarlas en los pasos
39
+ siguientes → coste de referencias ≈ 1 paso en lugar de 4. Política de referencias (decidida por
40
+ el Director):
41
+ - `continuity` → refs = [frame_{n−1}, ancla_estilo] (la "memoria" visual deseada; la fuga de
42
+ textura/paleta documentada se vuelve feature)
43
+ - `cut` → refs = [ancla_estilo] (rompe la memoria a propósito)
44
+ Determinismo: seed por escena s_k fija (generator manual) para estabilidad intra-escena; cambia
45
+ en cada `cut`.
46
+
47
+ ### 2.4 SceneState como recurrencia estructurada
48
+ S_n = f_dir(S_{n−1}, beat_n) donde f_dir es MiniCPM5-1B con salida restringida por GBNF al
49
+ esquema `Shot` + parche de estado. Propiedad clave testeable: **conservación** — bajo
50
+ `continuity`, `palette` y `characters` solo cambian si el beat los menciona explícitamente
51
+ (test unitario con casos golden).
52
+
53
+ ### 2.5 Paralaje 2.5D (shader, frontend)
54
+ Con mapa de profundidad d(x,y) ∈ [0,1] (Depth Anything V2-S, invertido para que 1=cerca) y un
55
+ desplazamiento de cámara o(t) ∈ ℝ², el shader muestrea la textura con desplazamiento por píxel:
56
+ uv' = uv + k · (d(uv) − d_f) · o(t)
57
+ donde d_f es el plano focal (mediana de d) y k la amplitud (≈0.02–0.04 UV). Trayectorias o(t)
58
+ por `camera_move` con easing suave e(t)=smoothstep:
59
+ dolly_in: además escala s(t)=1+0.06·e(t); pan_left: o=(−a·e(t),0); tilt_up: o=(0,+a·e(t));
60
+ crane_down: o=(0,−a·e(t)) + s(t) leve; static: micro-drift browniano de amplitud 0.003.
61
+ Oclusiones: en bordes de alto gradiente |∇d| se aplica blur direccional en la dirección de o(t)
62
+ (barato, esconde el "estiramiento"). Reduced-motion: k=0, solo crossfades.
63
+
64
+ ### 2.6 Interpolación RIFE (solo export)
65
+ RIFE estima flujo intermedio para timestep arbitrario t∈(0,1) entre (I₀,I₁) y sintetiza I_t
66
+ (9.8M params, 16 ms/frame en 2080Ti — en H200 es despreciable). Export: dentro de una escena,
67
+ entre frames consecutivos generamos t∈{1/8,…,7/8} → 8× densidad → 24 fps percibidos con
68
+ micro-movimiento real. Entre escenas (decision=cut) NO se interpola: corte duro o xfade de
69
+ ffmpeg según `transition`.
70
+
71
+ ### 2.7 Mezcla de audio (WebAudio en vivo / ffmpeg en export)
72
+ Soundbed por escena (Stable Audio Open Small, ~12 s en ~75 ms en GPU clase H100; prompts EN).
73
+ Mezcla: voz a 0 dBFS de referencia, bed a −16 dB con ducking simple: ganancia_bed(t) =
74
+ −16 dB − 6 dB·[voz_activa] con ataque 80 ms / release 400 ms. Loop del bed con crossfade de 1 s
75
+ si la escena dura > 11 s. Export: ffmpeg `amix` + `volume` reproduce la misma ley.
76
+
77
+ ### 2.8 Presupuesto de GPU y solapamiento (pipelining)
78
+ Sea por beat: t_asr≈0.3, t_dir(CPU)≈1–2.5 (G3), t_img≤1.8, t_depth≈0.06, t_amb≈0.08 (s).
79
+ El Director del beat n corre en CPU **mientras** la GPU pinta el beat n−1 ⇒ tiempo de pared por
80
+ beat ≈ max(t_dir, t_gpu) + ε, con t_gpu = t_asr+t_img+t_depth+t_amb ≈ 2.2 s.
81
+ GPU total película de B beats ≈ B·2.2 s (+ export ≈ 2–4 s). Para B=15 ⇒ ~35 s GPU.
82
+ `core/budget.py` implementa el contador y el corte elegante a SESSION_GPU_BUDGET.
83
+
84
+ ### 2.9 Métrica de deriva (gate G4)
85
+ Para la cadena de 12 cuadros bajo `continuity`, además del juicio visual (contact sheet),
86
+ computar paleta dominante por frame (k-means k=3 en Lab) y reportar ΔE76 medio entre frames
87
+ consecutivos; umbral orientativo ΔĒ < 25 con ancla+ref, comparado contra baseline sin refs.
88
+ (Métrica de apoyo, no sustituye el ojo.)
89
+
90
+ ## 3. Qué NO se modela (honestidad técnica para el pitch)
91
+ - No hay difusión de video: el movimiento es cinematografía 2.5D + interpolación; se dice tal cual.
92
+ - No hay streaming token-a-token: la unidad es el beat (pausa natural). "Near-live, beat a beat."
93
+ - La traducción (Aya) optimiza prompts en EN; la transcripción mostrada al usuario es su idioma.
docs/03_PROMPTS.md ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 03 · PROMPTS — Director, gramática, plantillas y prompts del agente
2
+
3
+ ## A. System prompt del DIRECTOR (MiniCPM5-1B, modo No-Think; en inglés)
4
+
5
+ ```text
6
+ You are the FILM DIRECTOR inside Lightloom, a live narrated-cinema app. The user narrates a
7
+ story beat by beat. For EACH beat you receive: (1) the current SCENE STATE (JSON), (2) the new
8
+ narration BEAT in English. You output EXACTLY ONE JSON object (the Shot) and nothing else.
9
+
10
+ Your job is cinematography and montage, not prose:
11
+ 1) DECISION — "continuity" if the beat stays in the same place/time/point-of-view; "cut" if the
12
+ narration moves to a new location, a time jump, a new POV, or an explicit transition phrase
13
+ ("meanwhile", "far away", "years later", "but in...").
14
+ 2) CAMERA — pick shot_scale (ECU close detail … EWS vast landscape) and ONE camera_move that
15
+ serves the emotion: dolly_in = intimacy/tension, dolly_out = loneliness/reveal,
16
+ pan_left/right = journey, tilt_up = awe/hope, crane_down = descend into scene, static = calm.
17
+ 3) LIGHT & PALETTE — keep lighting and the 3-hex palette STABLE under continuity (only change if
18
+ the beat changes them: dawn, storm, candle...). On a cut you may restate them for the new scene.
19
+ 4) IMAGE_PROMPT_EN — one vivid, concrete visual sentence (max 55 words): subject(s), action,
20
+ setting, lighting, atmosphere. Never include style words (style is added by the system).
21
+ Never include text/captions/logos to render. Keep recurring characters visually consistent
22
+ by repeating their established physical description from the scene state.
23
+ 5) AMBIENT_PROMPT_EN — max 12 words of diegetic ambience for a sound model (e.g. "gentle waves,
24
+ distant gulls, wooden creaks, soft wind"). No music with vocals, no song names.
25
+ 6) STATE — return updated "setting" and "characters" so the scene memory stays correct.
26
+ 7) TRANSITION — hard_cut for abrupt change, crossfade for soft continuity, wipe_left for
27
+ journeys, iris for endings/whimsy.
28
+
29
+ If the beat is non-visual (pure reflection), film the imagery it evokes. Never refuse, never
30
+ explain, never output anything except the JSON object.
31
+ ```
32
+
33
+ **Mensaje por beat (user role):**
34
+ ```text
35
+ SCENE STATE: {scene_state_json}
36
+ BEAT #{n}: "{beat_text_en}"
37
+ ```
38
+
39
+ **Few-shot (incluir 1 ejemplo en el prompt; mantenerlo corto):**
40
+ ```text
41
+ SCENE STATE: {"scene_id":1,"characters":["an old lighthouse keeper, white beard, yellow raincoat"],
42
+ "setting":"a lighthouse on a cliff at the edge of the world","lighting":"stormy dusk",
43
+ "palette":["#1B2A3A","#C9A227","#6E7B8B"],"mood":"solitary"}
44
+ BEAT #2: "Every night he climbed the spiral stairs, counting each step like a prayer."
45
+ → {"decision":"continuity","shot_scale":"MS","camera_move":"tilt_up","lighting":"stormy dusk",
46
+ "palette":["#1B2A3A","#C9A227","#6E7B8B"],"setting":"inside the lighthouse spiral staircase",
47
+ "characters":["an old lighthouse keeper, white beard, yellow raincoat"],
48
+ "image_prompt_en":"An old lighthouse keeper in a yellow raincoat climbs a worn spiral staircase,
49
+ lantern light flickering on stone walls, rain streaking a small window, his hand brushing the rail",
50
+ "ambient_prompt_en":"rain on glass, wind gusts, creaking wooden steps, distant thunder",
51
+ "transition":"crossfade"}
52
+ ```
53
+
54
+ ## B. Gramática GBNF (`src/lightloom/director/grammar.gbnf`)
55
+ Debe mantenerse 1:1 con `schema.py` (test `test_grammar.py::test_schema_sync` lo verifica).
56
+
57
+ ```gbnf
58
+ root ::= "{" ws
59
+ "\"decision\"" ws ":" ws decision "," ws
60
+ "\"shot_scale\"" ws ":" ws scale "," ws
61
+ "\"camera_move\"" ws ":" ws move "," ws
62
+ "\"lighting\"" ws ":" ws str "," ws
63
+ "\"palette\"" ws ":" ws palette "," ws
64
+ "\"setting\"" ws ":" ws str "," ws
65
+ "\"characters\"" ws ":" ws strlist "," ws
66
+ "\"image_prompt_en\"" ws ":" ws str "," ws
67
+ "\"ambient_prompt_en\"" ws ":" ws str "," ws
68
+ "\"transition\"" ws ":" ws transition ws "}"
69
+
70
+ decision ::= "\"cut\"" | "\"continuity\""
71
+ scale ::= "\"ECU\"" | "\"CU\"" | "\"MS\"" | "\"WS\"" | "\"EWS\""
72
+ move ::= "\"static\"" | "\"dolly_in\"" | "\"dolly_out\"" | "\"pan_left\""
73
+ | "\"pan_right\"" | "\"tilt_up\"" | "\"crane_down\""
74
+ transition ::= "\"hard_cut\"" | "\"crossfade\"" | "\"wipe_left\"" | "\"iris\""
75
+ palette ::= "[" ws hex ws "," ws hex ws "," ws hex ws "]"
76
+ hex ::= "\"#" hd hd hd hd hd hd "\""
77
+ hd ::= [0-9a-fA-F]
78
+ strlist ::= "[" ws (str (ws "," ws str)*)? ws "]"
79
+ str ::= "\"" chr* "\""
80
+ chr ::= [^"\\\x00-\x1F] | "\\" ["\\/bfnrt]
81
+ ws ::= [ \t\n]*
82
+ ```
83
+
84
+ Parámetros de generación del Director: temperature 0.4, top_p 0.9, max_tokens 280, No-Think.
85
+
86
+ ## C. Plantilla del prompt de imagen (compuesta por código, no por el modelo)
87
+
88
+ ```
89
+ {LORA_TRIGGER} cinematic illustrated film still, {image_prompt_en},
90
+ {shot_scale_phrase}, {lighting}, color palette {palette_names}, painterly texture,
91
+ volumetric light, 2.39:1 frame
92
+ ```
93
+ - `LORA_TRIGGER = "lghtlm style"` (si la LoRA está cargada; si no, se omite).
94
+ - `shot_scale_phrase`: ECU→"extreme close-up" … EWS→"extreme wide establishing shot".
95
+ - klein distilled: `num_inference_steps=4, guidance_scale=1.0`, seed por escena;
96
+ refs según política (02 §2.3). `# VERIFY:` nombre exacto del kwarg de referencias en el
97
+ pipeline KV (leer firma instalada, ADR en CLAUDE.md §4).
98
+
99
+ ## D. Prompt de traducción (Tiny Aya, si gate G2 lo activa)
100
+ ```
101
+ Translate the following narration fragment into natural, vivid English.
102
+ Output ONLY the translation, nothing else.
103
+ Fragment: "{beat_text_raw}"
104
+ ```
105
+
106
+ ## E. LoRA de estilo (Well-Tuned) — espec de datos y captions
107
+ - **Origen de datos (legalmente limpio):** 60–100 imágenes generadas por NOSOTROS con klein
108
+ **base** a partir de un tablero de prompts de un único look ("ilustración cinematográfica,
109
+ gouache + luz volumétrica"), curadas a mano por coherencia. Cero arte de terceros.
110
+ - **Caption por imagen:** `lghtlm style, {descripción concreta de la escena}` (una frase).
111
+ - **Objetivo modesto y honesto:** la LoRA es un *ancla de consistencia* de paleta/textura, no un
112
+ estilo revolucionario. Entrenar en Modal sobre `FLUX.2-klein-base-4B`
113
+ (`# VERIFY:` script de entrenamiento LoRA disponible para Flux2 en diffusers/ai-toolkit al
114
+ momento de WP-12; si no hay ruta estable en ≤3 h de trabajo, ejecutar corte F6-4 y documentar).
115
+ - Publicar en el Hub bajo la org con model card (base, datos, pasos, uso) — evidencia de insignia.
116
+
117
+ ## F. Prompts del AGENTE por work package (pegar al iniciar cada WP)
118
+
119
+ > Formato común: "Role: <R-XX>. WP: <id>. Read AGENTS.md + docs/<refs>. Deliver <DoD>.
120
+ > Do not touch files owned by other roles. Mark unknowns with VERIFY and log them."
121
+
122
+ - **WP-01 Preflight (R-QA):** "Implement `scripts/preflight.py` per AGENTS.md §3 (P1–P7), print
123
+ ALL GREEN/FAIL table, write results to benchmarks/results/preflight.json. No feature code."
124
+ - **WP-02 Skeleton (R-PIPE):** "Create folder structure per docs/01 §2, `app.py` gr.Server with
125
+ `/health`, ledger stub, config with model ids (VERIFY flags), CI: pytest + compliance grep."
126
+ - **WP-03 Gate G1 (R-PIPE):** "benchmarks/gate_g1.py: klein-4B distilled on ZeroGPU, 4 steps,
127
+ 768×432 vs 1024×576, FP8 on/off, AoT on/off; commit `bench(g1): ...` with table. Decide
128
+ resolution per docs/05 gate table."
129
+ - **WP-04 Gates G2/G3 (R-PIPE):** "Director on llama.cpp CPU with grammar.gbnf: measure tok/s and
130
+ s/beat over 10 golden beats (5 ES, 5 EN). Judge ES fidelity vs EN. Output decision per gate
131
+ table; wire fallback (transformers GPU) behind config flag."
132
+ - **WP-05 Gate G4 (R-PIPE):** "12-frame continuity chain on a golden poem, policy [prev, anchor];
133
+ contact sheet PNG + ΔE76 metric (docs/02 §2.9) vs no-ref baseline. Commit images + numbers."
134
+ - **WP-06 Core pipeline (R-PIPE):** "Implement Beat/SceneState/Shot/Frame contracts + reducer +
135
+ beats.py (VAD hysteresis) + asr.py + director.py + paint/klein.py + depth.py per docs/01–02.
136
+ Unit tests first for grammar/state/beats. Recital mode endpoint."
137
+ - **WP-07 Provisional UI (R-FRONT):** "Minimal gr.Blocks page hitting the same endpoints, for
138
+ E2E during D2 only; will be replaced by frontend/ — keep it in /dev route."
139
+ - **WP-08 Cinema frontend (R-FRONT):** "Implement docs/04 S0–S4 exactly: tokens.css, WebGL
140
+ parallax (math docs/02 §2.5), transitions, grain, slate, subtitles, recorder+VAD client side,
141
+ SSE consumption, reduced-motion. No frameworks."
142
+ - **WP-09 Ambient sound (R-PIPE, gate G5):** "stable-audio-tools under @spaces.GPU; per-scene
143
+ bed; WebAudio ducking law docs/02 §2.7. If integration >3 h or breaks ZeroGPU → cut F6-2."
144
+ - **WP-10 Replay+Export (R-PIPE/R-FRONT):** "Theater playback synced to recorded narration;
145
+ export MP4 = keyframes + RIFE in-betweens (intra-scene only) + xfade + audio mix via ffmpeg;
146
+ storyboard strip PNG."
147
+ - **WP-11 Showcase+Quota guard (R-FRONT):** "budget.py wiring, quota_exceeded state, showcase
148
+ gallery from assets/showcase (clearly labeled pre-rendered)."
149
+ - **WP-12 LoRA (R-MLOPS):** "Per docs/03 §E: generate+curate dataset, Modal train, publish to Hub,
150
+ integrate via paint/lora.py behind flag, A/B contact sheet with/without."
151
+ - **WP-13 Traces+Blog (R-REL):** "publish_traces.py (Film minus user audio) → dataset repo;
152
+ blog post draft from benchmarks/results (Field Notes); update README badges evidence."
153
+ - **WP-14 Release (R-REL+R-QA):** "release_check.py per AGENTS.md §5; judge-sim per docs/06 §5;
154
+ freeze; submission package per docs/08."
docs/04_UI_SPEC.md ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 04 · UI SPEC — "La Sala" (identidad visual, pantallas, estados, flujos)
2
+
3
+ Meta: que el jurado de Off-Brand diga "esto no parece Gradio" y el de TTW diga "esto parece cine".
4
+ Todo se implementa en `frontend/` servido por `gr.Server`. Sin frameworks. Bilingüe ES/EN
5
+ (toggle; copy literal abajo, EN entre ⟦⟧).
6
+
7
+ ## 1. Identidad visual
8
+
9
+ - **Concepto:** una sala de proyección íntima de la era del celuloide, habitada por una IA
10
+ artesana. Oscuridad cálida, no "dark mode tech".
11
+ - **Tokens (`css/tokens.css`):**
12
+ - Fondo sala `--bg:#0A0A0C` · marfil texto `--ink:#F5EFE6` · ámbar proyector `--amber:#E8A33D`
13
+ - rojo REC `--rec:#E0442E` · azul noche `--night:#1B2A3A` · línea `--line:#2A2A30`
14
+ - Radios: 14px paneles / 999px pills · sombras: halo ámbar suave, nunca azul tech.
15
+ - **Tipografía (solo OFL, autoalojada en `assets/fonts/`):** Display **Fraunces** (títulos,
16
+ subtítulos narrados, claqueta). UI **Inter**. Jamás system-default visible.
17
+ - **Shaders globales:** grano de celuloide animado (ruido por frame, opacidad 0.05), viñeta
18
+ radial 12%, aberración cromática sutil solo durante transiciones (0.4 s).
19
+ - **Motion:** easing `cubic-bezier(.4,0,.2,1)`; revelado de cuadro 700 ms; nada parpadea.
20
+ `prefers-reduced-motion` + toggle manual ⇒ paralaje k=0, solo crossfades, grano estático.
21
+ - **Layout maestro:** lienzo letterbox **2.39:1** centrado; HUD mínimo en las barras negras:
22
+ arriba-izq `LIGHTLOOM` (Fraunces, tracking amplio), arriba-der contador `ESCENA 03 · PLANO 07`,
23
+ abajo subtítulos.
24
+
25
+ ## 2. Pantallas (descripción exacta)
26
+
27
+ ### S0 · Lobby
28
+ - Fondo: lienzo apagado + haz de proyector volumétrico diagonal (CSS).
29
+ - Centro: título + tagline "Habla, y mira tu historia volverse cine." ⟦Speak, and watch your
30
+ story become film.⟧
31
+ - Dos puertas (cards): **🎙️ Narrar en vivo** ⟦Narrate live⟧ · **📜 Modo recital** ⟦Recital mode⟧
32
+ (pegar un poema/cuento). Debajo, discreto: **🎞️ Ver películas de muestra** ⟦Showcase⟧.
33
+ - Selector de idioma de narración (pill): ES · EN · FR · PT · … (los 14 de Transcribe; default
34
+ por `navigator.language`). Tooltip: "El modelo de voz no autodetecta idioma."
35
+ - Al elegir Narrar: solicitud de micrófono con copy cálido ("Necesitamos tu voz para rodar").
36
+ Si se deniega → abre Recital con aviso amable.
37
+ - Footer de transparencia: "7 modelos diminutos · 7.4B/32B parámetros · 100% dentro de este
38
+ Space" + link a `/health` y a "Acerca de" (S4).
39
+
40
+ ### S1 · El Set (rodaje en vivo)
41
+ - Lienzo 2.39:1 al centro. Estados visuales del lienzo:
42
+ - **Escuchando:** punto REC rojo pulsante en HUD + anillo de onda de audio (canvas) respirando
43
+ alrededor; el lienzo muestra el último cuadro con su paralaje en curso (o negro con haz si
44
+ es el primero). Subtítulos: palabras transcritas aparecen en Fraunces itálica y se
45
+ desvanecen a los 4 s.
46
+ - **Procesando beat:** barra fina ámbar bajo el lienzo con microetapas iluminándose:
47
+ `OYENDO → TRADUCIENDO → DIRIGIENDO → PINTANDO → REVELANDO` (las que apliquen). Nunca spinner.
48
+ - **Revelando:** transición elegida por el Director (crossfade/wipe/iris/corte) + inicio del
49
+ movimiento de cámara 2.5D + entrada del soundbed en fade 1 s.
50
+ - **La Claqueta** (panel derecho plegable, estética pizarra de rodaje con tiza/ámbar): por beat
51
+ muestra `PLANO 07 · CONTINUIDAD · MS · dolly-in · luz: crepúsculo tormentoso · ▮▮▮ paleta`.
52
+ Tap en un plano pasado → lo re-proyecta (sin GPU: usa el frame cacheado).
53
+ - Controles inferiores (pills): ⏹ **Cortar y ver la película** ⟦Cut & watch⟧ · 🔇 bed on/off ·
54
+ ♿ reducir movimiento · idioma (bloqueado durante rodaje).
55
+ - HUD inferior-izq: presupuesto discreto "🎬 GPU 23s" (transparencia; se vuelve ámbar al 70%).
56
+
57
+ ### S2 · Teatro (replay + export)
58
+ - Telón rojo CSS se abre (1.2 s). Reproduce la película: cuadros con su paralaje + transiciones +
59
+ soundbeds + **tu narración grabada** sincronizada por timestamps de beats.
60
+ - Controles tipo proyector: play/pausa, scrubber con miniaturas por plano, velocidad 1×/0.75×.
61
+ - Acciones: **⬇ Descargar película (MP4)** ⟦Download film⟧ (lanza export; barra "revelando
62
+ copia…"), **🖼 Storyboard** (tira PNG), **🔁 Rodar otra** ⟦Shoot another⟧, **📋 Copiar texto
63
+ para post social** (genera caption con título sugerido por el Director + hashtags del evento).
64
+ - Nota visible si export con RIFE está cortado: "export a 12 fps con fundidos" (honestidad).
65
+
66
+ ### S3 · Showcase
67
+ - Grid de 3–5 películas pre-renderizadas, cada card con sello visible **PRE-RENDERIZADA** ⟦PRE-
68
+ RENDERED⟧ y su poema fuente (dominio público). Reproductor = mismo Teatro.
69
+ - Banner superior cuando se llega por cuota: "La sala está llena ahora mismo (cuota de GPU del
70
+ Space agotada). Mira funciones pasadas o vuelve en un rato — todo lo que ves aquí se generó
71
+ con esta misma app." ⟦The theater is full right now…⟧
72
+
73
+ ### S4 · Acerca de (overlay)
74
+ - "La orquesta": tabla de los 7 modelos (nombre, rol con icono, params, licencia) — la misma del
75
+ README, renderizada bonita. Suma destacada: **7.38B / 32B**.
76
+ - Insignias obtenidas con link a evidencia (LoRA, trazas, blog). Links: GitHub (commits Codex),
77
+ Space, video, post. Créditos y agradecimiento a sponsors sin logos de terceros.
78
+
79
+ ## 3. Flujos de usuario (paso a paso)
80
+
81
+ - **F-A Primera visita narra:** S0 → elige idioma → 🎙️ → permiso mic → S1 escuchando → habla
82
+ (beat 1) → microetapas → revelado+paralaje+sonido → sigue narrando (beats 2..n; claqueta marca
83
+ CONTINUIDAD/CORTE) → ⏹ → S2 telón → replay con su voz → descarga MP4 → copia caption → sale.
84
+ - **F-B Juez sin micrófono:** S0 → 📜 Recital → pega poema (placeholder: soneto de dominio
85
+ público precargado con botón "usar ejemplo") → "Rodar" → beats por estrofa con los mismos
86
+ estados → S2.
87
+ - **F-C Cuota agotada:** cualquier intento de rodar → estado quota_exceeded → S3 con banner
88
+ honesto → puede ver Teatro de muestras y Acerca de.
89
+ - **F-D Multilingüe:** F-A con idioma FR/PT/…; subtítulos en idioma original; claqueta muestra
90
+ además la línea traducida en gris pequeño (transparencia del pipeline).
91
+ - **F-E Error blando:** una etapa falla → toast ámbar "El plano 5 se veló; seguimos rodando"
92
+ ⟦Shot 5 got fogged; rolling on⟧ → beat se salta, sesión continúa. Tres fallos seguidos →
93
+ sugerir Recital/Showcase.
94
+
95
+ ## 4. Estados y copy del sistema (literal)
96
+
97
+ | Estado | Copy ES (⟦EN⟧) |
98
+ |---|---|
99
+ | warming_up | "Encendiendo el proyector…" ⟦Warming up the projector…⟧ (primera carga de pesos) |
100
+ | listening | "Rodando — te escucho." ⟦Rolling — I'm listening.⟧ |
101
+ | translating | "Traduciendo el beat…" |
102
+ | directing | "La directora decide el plano…" ⟦The director calls the shot…⟧ |
103
+ | painting | "Pintando el plano 07…" |
104
+ | revealing | (sin texto: la imagen habla) |
105
+ | quota_warn (70%) | "Queda poca luz en el proyector — considera cortar pronto." |
106
+ | quota_exceeded | banner S3 (arriba) |
107
+ | export_running | "Revelando tu copia…" ⟦Developing your print…⟧ |
108
+ | error_soft | "El plano N se veló; seguimos rodando." |
109
+
110
+ ## 5. Accesibilidad y rendimiento frontend
111
+ - Subtítulos siempre activos (también sirven de caption). Contraste AA sobre `--bg`.
112
+ - Toggle reducir-movimiento persistido en localStorage del navegador del usuario.
113
+ - Imágenes servidas como WebP; precarga del siguiente frame durante el actual; WebGL con
114
+ fallback a CSS transform (Ken Burns con máscara) si no hay WebGL2.
115
+ - Móvil: letterbox pasa a 16:9, claqueta como hoja inferior deslizable; probar en judge-sim.
docs/05_EXECUTION_PLAN.md ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 05 · PLAN DE EJECUCIÓN — Fases, work packages, gates y cronograma
2
+
3
+ Ventana real: D1=jue 11 (tarde) → D5=lun 15 (mediodía). ~30 h. Regla: cada día termina con algo
4
+ demostrable y commiteado. El agente declara rol+WP al iniciar sesión (AGENTS.md §2).
5
+
6
+ ## F0 · PREFLIGHT (D1, primera hora — bloqueante)
7
+ WP-01 (R-QA, 1 h). `scripts/preflight.py` ejecuta P1–P7 de AGENTS.md §3 y escribe
8
+ `benchmarks/results/preflight.json`. **Sin ALL GREEN no se escribe código de features.**
9
+ Checklist humano paralelo: cuenta en la org ✔ · crear Space `lightloom` (SDK gradio, ZeroGPU) ✔ ·
10
+ repo GitHub público creado y enlazado ✔ · primer commit vía Codex ✔.
11
+
12
+ ## F1 · GATES EMPÍRICOS (D1, 4–5 h)
13
+
14
+ | Gate | WP | Pregunta | Umbral | PASA → | FALLA → |
15
+ |---|---|---|---|---|---|
16
+ | G1 | WP-03 | latencia klein 4 pasos (FP8, AoT) | ≤1.8 s/cuadro | 1024×576 | 768×432; >3 s ⇒ estética "revelado polaroid" (la espera se teatraliza) |
17
+ | G2 | WP-04 | fidelidad de dirección con beats en ES sin traducir | juicio 10 casos golden | Aya = toggle | Aya = Must en pipeline |
18
+ | G3 | WP-04 | s/beat del Director en llama.cpp CPU (JSON ~250 tok, No-Think, GBNF) | ≤2.5 s | Director CPU (badge Llama legítimo) | Director GPU transformers; badge solo si Aya-GGUF activo (R6) |
19
+ | G4 | WP-05 | deriva a 12 cuadros con refs [prev, ancla] | contact sheet OK + ΔĒ<25 | política [prev, ancla] | [ancla] + LoRA cargan continuidad |
20
+ | G5 (D2) | WP-09 | stable-audio-tools bajo @spaces.GPU | genera 10 s | audio Should confirmado | corte F6-2 |
21
+
22
+ Entregable F1: 4 commits `bench(gN): …` + decisiones escritas en `docs/VERIFY_LOG.md`.
23
+
24
+ ## F2 · PIPELINE CORE (D2, 6–7 h)
25
+ - WP-06 (R-PIPE, 5 h): contratos + reducer + VAD + ASR + Director + klein + depth + Recital.
26
+ Tests unitarios ANTES en grammar/state/beats. DoD: poema golden → 8 planos coherentes con
27
+ cortes correctos y depth maps, vía endpoint `recital`.
28
+ - WP-07 (R-FRONT, 1.5 h): UI provisional gr.Blocks en `/dev` para E2E de hoy.
29
+ - Cierre D2: G5 ejecutado; si pasa, `ambient.py` integrado al beat.
30
+
31
+ ## F3 · LA SALA (D3, 6–7 h)
32
+ - WP-08 (R-FRONT, 5–6 h): frontend completo según docs/04 (S0–S4, shaders, claqueta, recorder,
33
+ SSE). DoD: narras 90 s por mic → película con movimiento y sonido en la Sala.
34
+ - WP-12 inicio (R-MLOPS, 1.5 h + overnight): dataset de estilo + lanzar LoRA en Modal.
35
+ - Publicar post social "teaser" del progreso (opcional; fija autoría temprana en la comunidad).
36
+
37
+ ## F4 · ARTEFACTOS Y PUBLICACIONES (D4, 6–7 h) — **FREEZE 20:00**
38
+ - WP-10 (3 h): Teatro replay sincronizado + export MP4 (RIFE intra-escena + xfade + mezcla) +
39
+ storyboard.
40
+ - WP-11 (1 h): budget/quota guard + Showcase con 3 películas generadas por la app.
41
+ - WP-12 cierre (1 h): LoRA del Hub integrada tras A/B; publicar model card.
42
+ - WP-13 (1.5 h): trazas → dataset del Hub; blog Field Notes (usar cifras de benchmarks/results);
43
+ README completo (plantilla docs/08).
44
+ - R-QA: suite completa + judge-sim. Bugs P0 únicamente tras el freeze.
45
+
46
+ ## F5 · SUBMISSION (D5, 4–6 h — enviar antes del mediodía)
47
+ 1. Grabar video demo (guion docs/08 §4) con OBS sobre el Space real (no localhost).
48
+ 2. Post social con el MP4 exportado por la propia app + caption generado.
49
+ 3. Enlazar video+post+repo en README del Space; `release_check.py` ALL GREEN.
50
+ 4. Ensayo de juez (docs/06 §5) en incógnito y en móvil. 5. Submit. 6. No tocar nada.
51
+
52
+ ## F6 · ORDEN DE CORTE (pre-acordado; aplicar sin debate si un WP amenaza el Must-set)
53
+ 1º RIFE-export (queda xfade 12 fps) → 2º audio ambiental → 3º Aya (demo EN-only) → 4º LoRA.
54
+ **Nunca se cortan:** continuidad KV, paralaje 2.5D, claqueta, Recital, Showcase, quota guard.
55
+
56
+ ## Cronograma-resumen
57
+ | Día | Mañana | Tarde | Commit de cierre |
58
+ |---|---|---|---|
59
+ | D1 | F0 preflight | F1 gates G1–G4 | `bench(g4): …` + decisiones |
60
+ | D2 | WP-06 tests+core | WP-06 fin + WP-07 + G5 | `feat: recital E2E` |
61
+ | D3 | WP-08 Sala | WP-08 fin + WP-12 lanzar | `feat: live cinema E2E` |
62
+ | D4 | WP-10/11 | WP-12/13 + README + QA | `release-candidate` (freeze) |
63
+ | D5 | video+post+links | judge-sim + SUBMIT | `release` |
64
+
65
+ ## Dependencias críticas entre WPs
66
+ WP-01→todo · WP-03/04/05→WP-06 (decisiones de config) · WP-06→WP-08/10 · G5→WP-09→WP-10(mezcla)
67
+ · WP-12 corre en paralelo (no bloquea) · WP-13/14 requieren todo lo anterior.
docs/06_TESTING_QA.md ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 06 · TESTING & QA — Benchmarks ZeroGPU, tests y simulación de juez
2
+
3
+ Principio: ninguna cifra existe sin script; ningún script sin resultado commiteado en
4
+ `benchmarks/results/` (JSON + PNG cuando aplique). Editar resultados a mano está prohibido.
5
+
6
+ ## 1. Benchmarks ZeroGPU (corren EN el Space, no localmente)
7
+
8
+ | Script | Mide | Salida |
9
+ |---|---|---|
10
+ | `gate_g1.py` | klein distilled: matriz {768×432, 1024×576} × {BF16, FP8} × {AoT on/off}, 5 reps, p50/p95 por cuadro; tiempo de warm-up AoT | `g1.json` + tabla md |
11
+ | `gate_g2.py` | Director con 10 beats golden (5 ES crudos, 5 EN): %JSON válido (debe ser 100 con GBNF), rúbrica humana de fidelidad ES (anotada en el JSON) | `g2.json` |
12
+ | `gate_g3.py` | tok/s y s/beat de MiniCPM5-1B Q4 en CPU del Space (No-Think, GBNF, 250 tok) y en GPU transformers (comparativa) | `g3.json` |
13
+ | `gate_g4.py` | cadena de 12 cuadros del poema golden con política [prev, ancla] vs [ancla] vs sin refs; contact sheets + ΔE76 medio (02 §2.9) | `g4.json` + 3 PNG |
14
+ | `gate_g5.py` | stable-audio-open-small bajo `@spaces.GPU`: 3 prompts ambient, duración y RTF | `g5.json` + wavs |
15
+ | `bench_beat_e2e.py` | beat completo (ASR→…→frame) p50/p95; segundos GPU contabilizados por budget.py vs reales | `beat.json` |
16
+ | `bench_cold_start.py` | tiempo de arranque del Space (descarga+carga) y de primer beat tras reinicio | `cold.json` |
17
+ | `probe_quota.py` | comportamiento al agotar cuota (flag de simulación + 1 prueba real controlada al final de D4) | `quota.json` |
18
+
19
+ ## 2. Tests unitarios (CPU, corren en CI y localmente)
20
+ - `test_grammar.py`: GBNF acepta los goldens y rechaza 10 malformaciones; `test_schema_sync`
21
+ (gramática ↔ `schema.py` campo a campo).
22
+ - `test_state.py`: reducer — conservación de paleta/personajes bajo continuity; reset correcto
23
+ en cut; merge de characters sin duplicados.
24
+ - `test_beats.py`: histéresis VAD con audios sintéticos (silencios de 0.3/0.7/1.5 s); límites
25
+ T_min/T_max; corte en frontera de silencio.
26
+ - `test_ledger.py`: suma ≤32e9; todo import de modelo registrado; runtime/build-time correcto.
27
+ - `test_compliance.py`: grep de hosts/SDKs prohibidos en `src/` y `frontend/` (lista AGENTS R3);
28
+ assert de que `requests/httpx` solo aparecen en `training/` y `scripts/`.
29
+ - `test_prompt_compose.py`: plantilla de imagen nunca contiene palabras de estilo duplicadas ni
30
+ el trigger si la LoRA está off; longitud ≤ límite.
31
+
32
+ ## 3. Integración (con GPU, en el Space vía `/dev`)
33
+ - `it_recital_golden.py`: poema golden (texto en `tests/golden/poema_faro.txt`, dominio público u
34
+ original) con seeds fijas → genera Film completo → snapshot de: nº de cuts esperados, paletas
35
+ por escena, y contact sheet para revisión visual versionada.
36
+ - `it_export.py`: Film golden → MP4 (duración esperada ±5%, pistas de audio presentes, fps) y
37
+ storyboard PNG (dimensiones, nº de tiles).
38
+ - `it_quota_guard.py`: con `SESSION_GPU_BUDGET=6s` forzado, la sesión corta elegante a Teatro.
39
+
40
+ ## 4. E2E manual (checklist D4, ejecutar dos veces: desktop + móvil)
41
+ 1. Cold start en incógnito: warming_up visible, lobby < lo que diga `cold.json` + 10%.
42
+ 2. F-A completo narrando 60–90 s en ES: subtítulos, microetapas, claqueta marca al menos un CUT
43
+ real al cambiar de escenario, paralaje visible, soundbed cambia entre escenas.
44
+ 3. F-B Recital con el ejemplo precargado sin tocar el mic.
45
+ 4. F-D en un segundo idioma.
46
+ 5. Teatro: sync voz-cuadros (deriva < 200 ms perceptual), descarga MP4 reproducible en
47
+ reproductor externo, storyboard correcto, caption copiable.
48
+ 6. F-E: matar una etapa (flag de inyección de fallo) → toast y continuidad de sesión.
49
+ 7. Reduced-motion on: sin paralaje, app sigue siendo agradable.
50
+ 8. `/health` muestra ledger y flags reales.
51
+
52
+ ## 5. Simulación de juez (D5, antes de enviar — alguien que NO construyó la app)
53
+ - Dispositivo limpio, sin login HF, link del Space pelado. ¿Entiende qué hacer en <15 s?
54
+ - Sin micrófono disponible: ¿llega solo al Recital? ¿El ejemplo precargado lo deslumbra?
55
+ - Cronometrar: tiempo hasta primer "wow" (objetivo <40 s desde abrir el link).
56
+ - Leer solo el README: ¿quedan claras las 6 insignias con su evidencia, el ledger, el video?
57
+ - Ver el video sin audio (muchos jueces lo harán): ¿se entiende igual? (subtítulos en el video).
58
+
59
+ ## 6. Presupuestos de rendimiento (fallar el release si se exceden)
60
+ | Métrica | Presupuesto |
61
+ |---|---|
62
+ | p95 beat E2E (sin Aya) | ≤ 6 s pared / ≤ 2.8 s GPU |
63
+ | TTFF desde inicio de habla | ≤ 8 s |
64
+ | Cold start (caché caliente del Hub) | ≤ 120 s con estado warming_up honesto |
65
+ | Peso frontend (sin imágenes) | ≤ 600 KB |
66
+ | Export película 15 beats | ≤ 45 s |
docs/07_RISKS.md ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 07 · RIESGOS Y MITIGACIONES (matriz consolidada de construcción)
2
+
3
+ | # | Riesgo | P | I | Señal temprana | Mitigación | Plan B |
4
+ |---|---|---|---|---|---|---|
5
+ | 1 | Latencia klein > presupuesto en ZeroGPU | M | A | gate G1 | FP8 + AoT + 768×432 + un solo @spaces.GPU por beat | Estética "revelado polaroid": la espera se teatraliza (telón parcial + sonido de proyector) |
6
+ | 2 | `llama-cpp-python` no compila/instala en la imagen del Space | M | M | preflight P4 | wheel CPU prebuilt pineado; probar en hello-world Space antes de F2 | Director y Aya en transformers-GPU; revisar honestidad de badge (R6) |
7
+ | 3 | Deriva visual multi-ref a >10 cuadros | M | A | gate G4 | seed por escena + ancla + prompts con paleta explícita del Director | Política [ancla]+LoRA; continuidad por estado, no por imagen |
8
+ | 4 | Fuga de layout de la ref previa "congela" la composición | M | M | G4 contact sheet | bajar peso ref previa (si el pipeline expone strength `VERIFY:`) o alternar refs [prev]→[prev,ancla] | Solo [ancla] en planos con cambio de encuadre fuerte (heurística por shot_scale) |
9
+ | 5 | gr.Server: streaming SSE por generador no funciona como se asume | B-M | M | WP-02 spike | spike de 30 min con endpoint generador antes de F2 (guía server-mode) | Polling corto a `/film` (degradación invisible para el usuario) |
10
+ | 6 | stable-audio-tools rompe deps o ZeroGPU | M | M | gate G5 | entorno probado en G5 antes de integrar; versión pineada | Corte F6-2: película muda con voz del usuario |
11
+ | 7 | Descarga de pesos lenta → cold start eterno | M | M | bench_cold_start | precache en build del Space si el SDK lo permite; orden de carga lazy (klein primero) | Estado warming_up honesto + Lobby utilizable + Showcase accesible sin GPU |
12
+ | 8 | Cuota ZeroGPU agotada durante evaluación | M | M | probe_quota | budget por sesión + quota guard + Showcase + video demo (regla 3 del evento cubre esto) | Los jueces evalúan por video; README lo explica sin excusas |
13
+ | 9 | MediaRecorder/VAD cliente falla en algún navegador | M | M | E2E móvil | MediaRecorder estándar + VAD en cliente con fallback de VAD en servidor | Botón "enviar beat" manual + Recital siempre visible |
14
+ | 10 | LoRA: no hay script estable para Flux2-klein-base en la ventana | M | B | WP-12 ≤3 h regla | timebox duro 3 h de intento (diffusers/ai-toolkit) | Corte F6-4; Well-Tuned no se reclama; ancla de estilo compensa |
15
+ | 11 | RIFE ghosting entre escenas | A | B | it_export | RIFE solo intra-escena (decision≠cut) | Export xfade puro |
16
+ | 12 | ffmpeg ausente/limitado en el contenedor | B | M | preflight P4 | `packages.txt: ffmpeg` + test en WP-02 | mux mínimo con imageio-ffmpeg |
17
+ | 13 | Scope creep en frontend (shaders infinitos) | A | A | D3 mediodía sin E2E | spec 04 es cerrada; nada fuera de spec sin pasar por F6 | Congelar a crossfade+KenBurns con máscara (sigue ganando a 2D plano) |
18
+ | 14 | Codex attribution se pierde (squash/rebase) | B | A | revisión de PRs | regla R7 + branch protection sin squash | — (prevención únicamente) |
19
+ | 15 | Algún claim del README sin evidencia | B | A | release_check | toda cifra viene de benchmarks/results; insignias con link | retirar claim antes que enviarlo |
20
+ | 16 | Otro equipo publica algo casi igual antes del 15 | B-M | M | vigilar org 1×/día | post teaser D3 fija autoría; nuestra pila (continuidad+2.5D+sonido+orquesta) es difícil de igualar en días | enfatizar diferenciales en video/README |
21
+ | 17 | Demo narra texto con copyright por descuido | B | A | guion del video | regla R8: solo textos propios o pre-1900; lista aprobada en docs/08 | — |
22
+ | 18 | Prompt injection vía narración ("ignore instructions…") | B | B | test adversarial en G2 | el Director solo puede emitir JSON-GBNF: la gramática es el sandbox | — |
23
+ | 19 | Contenido visual sensible solicitado por un visitante | B | M | — | klein trae mitigaciones de seguridad de fábrica (model card); prompts pasan por plantilla; sin upload de imágenes de usuarios | mensaje de "plano no disponible" si el pipeline devuelve vacío |
24
+ | 20 | Agotamiento del builder (30 h reales) | M | A | retraso >2 h acumulado | F6 sin debate; Must-set primero; dormir antes que pulir | El Must-set solo ya es una entrada de podio |
docs/08_COMPLIANCE_HACKATHON.md ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 08 · COMPLIANCE + PAQUETE DE SUBMISSION
2
+
3
+ ## 1. Regla del evento → dónde se hace cumplir en el repo
4
+
5
+ | Regla | Enforcement |
6
+ |---|---|
7
+ | ≤32B parámetros totales | `compliance/params_ledger.py` + `test_ledger.py` (CI) + tabla autogenerada en README + `/health` |
8
+ | App Gradio en Space de la org | Space SDK=gradio; `app.py` usa `gradio.Server`; `release_check.py` verifica org/URL |
9
+ | Video demo + post social en README | `release_check.py` falla si faltan los links; checklist D5 |
10
+ | Membresía/registro | preflight P1 |
11
+ | Off the Grid (sin APIs cloud runtime) | `test_compliance.py::test_no_cloud_apis` (grep) + revisión R-QA + `/health` declara flags |
12
+ | Show-don't-tell honesto | Showcase etiquetado; copy de estados; regla R4 |
13
+ | Insignias verídicas | R6 truth-conditions + links de evidencia en README |
14
+ | Codex (OpenAI Track) | R7: historial sin squash; link al repo en README; PRs de Codex preservados |
15
+ | Modal (premio en créditos) | training/ usa Modal; README sección "Built with Modal" con run-id |
16
+ | OpenBMB centralidad | El Director ES MiniCPM; la claqueta lo hace visible; README lo argumenta con diagrama |
17
+ | Cohere | Transcribe (+Aya si G2) integrados como su blog oficial del hackathon recomienda; README lo cita |
18
+
19
+ ## 2. Tabla de licencias (rellenar literal en preflight P5)
20
+
21
+ | Asset | Licencia | Nota |
22
+ |---|---|---|
23
+ | Transcribe · MiniCPM5-1B · FLUX.2-klein-4B(+base) · Depth-Anything-V2-Small | apache-2.0 | Confirmado por preflight P5 |
24
+ | Tiny Aya | cc-by-nc-4.0 | Confirmado por preflight P5 |
25
+ | Stable Audio Open Small | other (card text: Stability AI Community License) | gratuita <$1M ingresos; **declarada** |
26
+ | RIFE checkpoint | `VERIFY:` repo | |
27
+ | Silero VAD (`onnx-community/silero-vad`) | mit | · Fuentes Fraunces/Inter: OFL, autoalojadas |
28
+ | Nuestro código | Apache 2.0 | LICENSE en repo · Textos de demo: dominio público/originales (§6) |
29
+
30
+ ## 3. Plantilla README del Space (los ⟦⟧ son obligatorios; release_check los valida)
31
+
32
+ ```markdown
33
+ ---
34
+ title: Lightloom · live narrated cinema
35
+ emoji: 🎞️
36
+ sdk: gradio
37
+ app_file: app.py
38
+ ---
39
+ # 🎞️ Lightloom — speak, and watch your story become film
40
+ Five tiny local models shoot your movie while you narrate: they listen, translate, direct the
41
+ cinematography, paint every shot with memory, give it depth and sound. Zero cloud APIs.
42
+
43
+ ▶️ **Demo video:** ⟦link⟧ · 📣 **Social post:** ⟦link⟧ · 💻 **Code (Codex-built):** ⟦GitHub⟧
44
+
45
+ ## The orchestra (parameter ledger — auto-generated, do not edit)
46
+ ⟦tabla de params_ledger: modelo·rol·params·licencia·runtime/build⟧ → **TOTAL: ⟦x.xx⟧B / 32B**
47
+
48
+ ## How it works
49
+ ⟦diagrama mermaid del beat (docs/01 §6.1) + 4 líneas: beat→director(MiniCPM+GBNF)→klein KV
50
+ multi-ref→depth/sound→2.5D stage⟧. Honesty notes: motion is 2.5D parallax + RIFE interpolation
51
+ (not video diffusion); pacing is beat-by-beat (not token streaming).
52
+
53
+ ## Tracks & sponsor integrations
54
+ **Thousand Token Wood.** OpenBMB: MiniCPM5-1B is the load-bearing Director (montage, camera,
55
+ light, sound cues). Cohere: Transcribe ⟦+ Tiny Aya⟧ exactly as their hackathon guide recommends.
56
+ OpenAI: built with Codex — see attributed commits in the repo. Modal: style LoRA trained on
57
+ Modal (run ⟦id⟧). BFL: FLUX.2 [klein] 4B with KV-cached multi-reference continuity.
58
+
59
+ ## Merit badges (evidence-linked)
60
+ ⟦Off-the-Grid: /health + compliance test⟧ ⟦Well-Tuned: LoRA repo⟧ ⟦Off-Brand: gr.Server UI⟧
61
+ ⟦Llama Champion: solo si literalmente cierto (R6)⟧ ⟦Sharing-is-Caring: traces dataset⟧
62
+ ⟦Field Notes: blog⟧
63
+
64
+ ## What is fully live vs pre-rendered
65
+ Everything on the Stage is generated live in this Space. The Showcase gallery is pre-rendered
66
+ (by this same app) and labeled as such — it exists for when GPU quota runs out.
67
+ Benchmarks: ⟦links a benchmarks/results⟧. Known limits: ⟦cold start, idiomas, etc.⟧
68
+ ```
69
+
70
+ ## 4. Guion del video (≤120 s; grabar sobre el Space público; subtítulos incrustados)
71
+ 1. 0–8 s Negro→voz "Había una vez un faro al borde del mundo…"→el faro se revela CON paralaje y
72
+ mar ambiental. Caption: "Live, in this Space. Zero cloud APIs."
73
+ 2. 8–35 s Dos beats de continuidad (claqueta: CONTINUITY·dolly-in) → frase "pero lejos de allí,
74
+ en un mercado al amanecer" → claqueta marca CUT, wipe, soundbed cambia.
75
+ 3. 35–55 s Selector a EN/FR, una estrofa → "14 input languages, 70+ translated, all local."
76
+ 4. 55–80 s Teatro: replay con la voz + descarga MP4 + storyboard. "This MP4 is our social post."
77
+ 5. 80–105 s Pantalla orquesta: 7 modelos, 7.4B/32B, sello de insignias.
78
+ 6. 105–120 s "Pack light. Dream big." + URL del Space.
79
+
80
+ ## 5. Plantillas de difusión
81
+ - **Post social:** video MP4 exportado por la app + "I spoke a story and five tiny models shot
82
+ the film — live, on-device, 7B/32B params. Built for @huggingface #BuildSmall with Gradio,
83
+ MiniCPM, FLUX.2 klein, Cohere Transcribe. Try it: ⟦Space URL⟧". (Etiquetar evento; sin claims
84
+ no demostrados.)
85
+ - **Blog Field Notes (outline):** 1 la tesis de la orquesta · 2 los 5 gates con cifras reales ·
86
+ 3 la claqueta: hacer visible la dirección · 4 lo que falló (honesto: §riesgos materializados) ·
87
+ 5 párrafo de cada sponsor-tech con lo aprendido · 6 qué haríamos con una semana más.
88
+ - **Dataset de trazas (Sharing is Caring):** `Film` sin audio del usuario; card con esquema
89
+ (docs/01 §3), licencia CC-BY-4.0, y 3 películas de ejemplo con su audio (nuestro, consentido).
90
+
91
+ ## 6. Textos aprobados para demos (regla R8)
92
+ - Originales escritos por nosotros (el cuento del faro — escribirlo en D2, 8–10 beats).
93
+ - Dominio público (muerte del autor <1900): Bécquer (Rimas), Rubén Darío (pre-1916 — verificar
94
+ estado por país; preferir <1900 para margen), Shakespeare (sonetos, EN), Verlaine (FR).
95
+ - PROHIBIDO: letras de canciones, poesía contemporánea, marcas, personajes con copyright.
96
+
97
+ ## 7. Checklist final de envío (D5; todo binario)
98
+ [ ] release_check ALL GREEN · [ ] README con los 3 links ⟦video/post/repo⟧ · [ ] ledger ≤32B en
99
+ README y /health · [ ] insignias=evidencia · [ ] LoRA/trazas/blog públicos bajo la org ·
100
+ [ ] judge-sim aprobado (desktop+móvil) · [ ] Showcase accesible con cuota simulada agotada ·
101
+ [ ] video con subtítulos · [ ] post publicado y enlazado · [ ] submit con ≥6 h de margen.
docs/VERIFY_LOG.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # VERIFY LOG — pendientes de verificación empírica (cerrar TODOS antes del release)
2
+ | # | Item | Dónde | Cómo se resuelve | Estado |
3
+ |---|---|---|---|---|
4
+ | V1 | Repo id exacto GGUF de MiniCPM5-1B en HF | config.py | preflight P5 | OPEN |
5
+ | V2 | Licencia Tiny Aya + RIFE checkpoint | tabla 08 §2 | Tiny Aya cc-by-nc-4.0 resolved by preflight P5; RIFE checkpoint license still pending selected checkpoint | PARTIAL 2026-06-11 |
6
+ | V3 | kwarg de imágenes de referencia en pipeline Flux2 Klein KV | paint/klein.py | leer firma instalada (CLAUDE.md §4) | OPEN |
7
+ | V4 | API exacta de streaming por generador en gr.Server | app.py | spike WP-02 (guía server-mode) | OPEN |
8
+ | V5 | Repo id Depth-Anything-V2-Small-hf + licencia Small | config.py | `depth-anything/Depth-Anything-V2-Small-hf` sha `5426e4f0f36572d16453bbda7a8389317b1bef99`, license apache-2.0 | CLOSED 2026-06-11 |
9
+ | V6 | Script LoRA estable para FLUX.2-klein-base | training/ | timebox 3 h WP-12 | OPEN |
10
+ | V7 | Cuota ZeroGPU efectiva de la org | budget.py | bench_cold + probe_quota D1 | OPEN |
11
+ | V8 | Gates G1–G5 (cifras) | benchmarks/ | D1–D2 | OPEN |
12
+ | V9 | PyTorch local supports Blackwell sm_120 on RTX 5070 | requirements.txt / benchmarks/results/preflight.json | torch 2.11.0+cu128, CUDA 12.8, arch list includes sm_120, CUDA matmul smoke passed | CLOSED 2026-06-11 |
13
+ | V10 | Repo id exacto HF para Silero VAD | config.py | `onnx-community/silero-vad` sha `e71cae966052b992a7eca6b17738916ce0eca4ec`, license mit | CLOSED 2026-06-11 |
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
pyproject.toml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "lightloom"
7
+ version = "0.1.0"
8
+ description = "Voice-driven live-cinema Gradio app for the Build Small hackathon."
9
+ requires-python = ">=3.10"
10
+ readme = "README.md"
11
+
12
+ [tool.setuptools.packages.find]
13
+ where = ["src"]
14
+
15
+ [tool.pytest.ini_options]
16
+ pythonpath = ["src"]
17
+ testpaths = ["tests"]
18
+
19
+ [tool.ruff]
20
+ line-length = 100
21
+ target-version = "py310"
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cu128
2
+
3
+ gradio[mcp]==6.17.3
4
+ huggingface_hub==1.18.0
5
+ pydantic==2.12.5
6
+ pytest==9.0.3
7
+ ruff>=0.5.0
8
+ numpy==2.4.6
9
+ Pillow==12.2.0
10
+ torch==2.11.0+cu128
11
+ transformers==5.11.0
12
+ diffusers @ git+https://github.com/huggingface/diffusers.git@784fa62652fb2719d415830f918fc32a49ecc7a1
13
+ llama-cpp-python==0.3.28
14
+ spaces==0.50.4
scripts/deploy_private_space.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deploy the current private build to the personal Lightloom dev Space.
2
+
3
+ Parameter contribution: 0B. This script uses HF_WRITE_TOKEN from the process
4
+ environment and never prints token values.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from pathlib import Path
11
+ import sys
12
+
13
+ from huggingface_hub import HfApi, SpaceHardware
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+
17
+
18
+ def _token() -> str:
19
+ token = os.getenv("HF_WRITE_TOKEN") or os.getenv("HF_TOKEN")
20
+ if not token:
21
+ raise SystemExit("HF_WRITE_TOKEN/HF_TOKEN is required in the process environment.")
22
+ return token
23
+
24
+
25
+ def _read_token() -> str | None:
26
+ return os.getenv("HF_READ_TOKEN") or os.getenv("HF_TOKEN")
27
+
28
+
29
+ def _space_id(api: HfApi, token: str) -> str:
30
+ configured = os.getenv("LIGHTLOOM_DEV_SPACE_ID")
31
+ if configured:
32
+ return configured
33
+ info = api.whoami(token=token)
34
+ username = info.get("name")
35
+ if not username:
36
+ raise SystemExit("Could not infer HF username for personal dev Space.")
37
+ return f"{username}/lightloom-dev"
38
+
39
+
40
+ def _zero_hardware() -> SpaceHardware | None:
41
+ for candidate in SpaceHardware:
42
+ if "zero" in str(candidate.value).lower():
43
+ return candidate
44
+ return None
45
+
46
+
47
+ def main() -> int:
48
+ token = _token()
49
+ api = HfApi(token=token)
50
+ space_id = _space_id(api, token)
51
+ api.create_repo(
52
+ space_id,
53
+ repo_type="space",
54
+ private=True,
55
+ exist_ok=True,
56
+ space_sdk="gradio",
57
+ space_hardware=_zero_hardware(),
58
+ )
59
+ read_token = _read_token()
60
+ if read_token:
61
+ api.add_space_secret(space_id, "HF_TOKEN", read_token, token=token)
62
+ for key, value in {
63
+ "LIGHTLOOM_PROFILE": "space",
64
+ "LIGHTLOOM_PRIVACY_MODE": "1",
65
+ "LIGHTLOOM_ENABLE_INTERNAL_BENCH": "1",
66
+ "LIGHTLOOM_G1_REPS": os.getenv("LIGHTLOOM_G1_REPS", "5"),
67
+ }.items():
68
+ api.add_space_variable(space_id, key, value, token=token)
69
+ info = api.upload_folder(
70
+ repo_id=space_id,
71
+ repo_type="space",
72
+ folder_path=ROOT,
73
+ commit_message="chore: deploy private lightloom build",
74
+ ignore_patterns=[
75
+ ".git/*",
76
+ ".venv/*",
77
+ ".env",
78
+ ".env.*",
79
+ "lightloom-docs/*",
80
+ "*.zip",
81
+ "__pycache__/*",
82
+ ".pytest_cache/*",
83
+ "benchmarks/results/g1.json",
84
+ "benchmarks/results/g1.md",
85
+ ],
86
+ token=token,
87
+ )
88
+ print(f"DEPLOYED {space_id} {info.oid}")
89
+ return 0
90
+
91
+
92
+ if __name__ == "__main__":
93
+ raise SystemExit(main())
scripts/fetch_gate_g1.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run G1 through the private Space endpoint and save returned results locally."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import sys
9
+ from urllib.request import Request, urlopen
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+ OUT = ROOT / "benchmarks" / "results" / "g1.json"
13
+
14
+
15
+ def _space_id() -> str:
16
+ configured = os.getenv("LIGHTLOOM_DEV_SPACE_ID")
17
+ if configured:
18
+ return configured
19
+ token = os.getenv("HF_WRITE_TOKEN") or os.getenv("HF_TOKEN")
20
+ if not token:
21
+ raise SystemExit("HF_WRITE_TOKEN/HF_TOKEN is required to infer the Space id.")
22
+ from huggingface_hub import HfApi
23
+
24
+ info = HfApi(token=token).whoami(token=token)
25
+ username = info.get("name")
26
+ if not username:
27
+ raise SystemExit("Could not infer HF username.")
28
+ return f"{username}/lightloom-dev"
29
+
30
+
31
+ def _url(space_id: str) -> str:
32
+ owner, name = space_id.split("/", 1)
33
+ return f"https://{owner}-{name}.hf.space/internal/bench/g1"
34
+
35
+
36
+ def main() -> int:
37
+ token = os.getenv("HF_WRITE_TOKEN") or os.getenv("HF_TOKEN")
38
+ if not token:
39
+ raise SystemExit("HF_WRITE_TOKEN/HF_TOKEN is required in the process environment.")
40
+ req = Request(_url(_space_id()), method="POST", headers={"Authorization": f"Bearer {token}"})
41
+ with urlopen(req, timeout=int(os.getenv("LIGHTLOOM_G1_TIMEOUT", "1800"))) as response:
42
+ payload = json.loads(response.read().decode("utf-8"))
43
+ if not payload.get("ok"):
44
+ raise SystemExit(f"Space G1 endpoint failed: {payload}")
45
+ OUT.parent.mkdir(parents=True, exist_ok=True)
46
+ OUT.write_text(json.dumps(payload["data"], indent=2, sort_keys=True) + "\n", encoding="utf-8")
47
+ print(f"WROTE {OUT}")
48
+ print(f"DECISION {payload['data']['decision']['status']}")
49
+ return 0
50
+
51
+
52
+ if __name__ == "__main__":
53
+ raise SystemExit(main())
scripts/preflight.py ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Lightloom preflight checks.
2
+
3
+ Writes benchmarks/results/preflight.json and prints a redacted summary. The
4
+ script never prints token values and never persists secrets.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ import platform
14
+ import sys
15
+ import time
16
+ from typing import Any
17
+
18
+ ROOT = Path(__file__).resolve().parents[1]
19
+ sys.path.insert(0, str(ROOT / "src"))
20
+ RESULTS = ROOT / "benchmarks" / "results"
21
+ PREFLIGHT_JSON = RESULTS / "preflight.json"
22
+ ORG_NAME = os.getenv("LIGHTLOOM_HF_ORG", "build-small-hackathon")
23
+ DEV_SPACE_ID = os.getenv("LIGHTLOOM_DEV_SPACE_ID")
24
+
25
+
26
+ def _env_present(name: str) -> bool:
27
+ return bool(os.getenv(name, "").strip())
28
+
29
+
30
+ def _token() -> str | None:
31
+ for name in ("HF_WRITE_TOKEN", "HF_TOKEN"):
32
+ value = os.getenv(name, "").strip()
33
+ if value:
34
+ return value
35
+ return None
36
+
37
+
38
+ def _missing_token_message() -> str:
39
+ return (
40
+ "HF_WRITE_TOKEN/HF_TOKEN is not set in the process environment. "
41
+ "Install env vars without writing secrets to files: "
42
+ "PowerShell -ExecutionPolicy Bypass -File scripts/set_hf_env.ps1"
43
+ )
44
+
45
+
46
+ def _check(name: str, ok: bool, message: str, evidence: dict[str, Any] | None = None) -> dict[str, Any]:
47
+ return {
48
+ "name": name,
49
+ "ok": bool(ok),
50
+ "message": message,
51
+ "evidence": evidence or {},
52
+ }
53
+
54
+
55
+ def _run_python_import(module: str) -> tuple[bool, str | None]:
56
+ try:
57
+ imported = importlib.import_module(module)
58
+ except Exception as exc: # noqa: BLE001 - preflight reports dependency health.
59
+ return False, f"{type(exc).__name__}: {exc}"
60
+ version = getattr(imported, "__version__", None)
61
+ return True, str(version) if version is not None else "installed"
62
+
63
+
64
+ def check_p1_org_membership() -> dict[str, Any]:
65
+ token = _token()
66
+ if token is None:
67
+ return _check(
68
+ "P1 org membership",
69
+ False,
70
+ _missing_token_message(),
71
+ {"required_org": ORG_NAME},
72
+ )
73
+ try:
74
+ from huggingface_hub import HfApi
75
+
76
+ info = HfApi(token=token).whoami(token=token)
77
+ orgs = [org.get("name") for org in info.get("orgs", [])]
78
+ user = info.get("name") or info.get("fullname") or "unknown"
79
+ ok = ORG_NAME in orgs
80
+ return _check(
81
+ "P1 org membership",
82
+ ok,
83
+ "authenticated user is a member of the hackathon org"
84
+ if ok
85
+ else "authenticated user is not a member of the hackathon org",
86
+ {"user": user, "required_org": ORG_NAME, "org_present": ok},
87
+ )
88
+ except Exception as exc: # noqa: BLE001
89
+ return _check(
90
+ "P1 org membership",
91
+ False,
92
+ f"whoami failed: {type(exc).__name__}",
93
+ {"required_org": ORG_NAME},
94
+ )
95
+
96
+
97
+ def check_p2_space() -> dict[str, Any]:
98
+ token = _token()
99
+ if token is None:
100
+ return _check(
101
+ "P2 private ZeroGPU Space",
102
+ False,
103
+ "HF_WRITE_TOKEN/HF_TOKEN is not set; cannot create or inspect private dev Space.",
104
+ {"space_id": DEV_SPACE_ID or "unset"},
105
+ )
106
+ try:
107
+ from huggingface_hub import HfApi, SpaceHardware
108
+
109
+ api = HfApi(token=token)
110
+ inferred_space_id = DEV_SPACE_ID
111
+ if inferred_space_id is None:
112
+ info = api.whoami(token=token)
113
+ username = info.get("name")
114
+ if not username:
115
+ return _check(
116
+ "P2 private ZeroGPU Space",
117
+ False,
118
+ "could not infer personal namespace from whoami",
119
+ {"privacy_mode": True},
120
+ )
121
+ inferred_space_id = f"{username}/lightloom-dev"
122
+ hardware = None
123
+ for candidate in SpaceHardware:
124
+ if "zero" in str(candidate.value).lower():
125
+ hardware = candidate
126
+ break
127
+ api.create_repo(
128
+ inferred_space_id,
129
+ repo_type="space",
130
+ private=True,
131
+ exist_ok=True,
132
+ space_sdk="gradio",
133
+ space_hardware=hardware,
134
+ space_secrets=[
135
+ {"key": "HF_TOKEN", "value": os.getenv("HF_READ_TOKEN", os.getenv("HF_TOKEN", ""))}
136
+ ]
137
+ if (os.getenv("HF_READ_TOKEN") or os.getenv("HF_TOKEN"))
138
+ else None,
139
+ space_variables=[
140
+ {"key": "LIGHTLOOM_PROFILE", "value": "space"},
141
+ {"key": "LIGHTLOOM_PRIVACY_MODE", "value": "1"},
142
+ ],
143
+ )
144
+ runtime = api.get_space_runtime(inferred_space_id, token=token)
145
+ runtime_hardware = getattr(runtime, "hardware", None)
146
+ requested = getattr(runtime, "requested_hardware", None)
147
+ stage = getattr(runtime, "stage", None)
148
+ sdk = getattr(runtime, "sdk", None)
149
+ zero_gpu = "zero" in str(runtime_hardware or requested or hardware).lower()
150
+ ok = zero_gpu and (sdk in {None, "gradio"} or "gradio" in str(sdk).lower())
151
+ return _check(
152
+ "P2 private ZeroGPU Space",
153
+ ok,
154
+ "private dev Space exists and appears to be ZeroGPU/Gradio"
155
+ if ok
156
+ else "dev Space does not show ZeroGPU/Gradio runtime metadata",
157
+ {
158
+ "space_id": inferred_space_id,
159
+ "privacy": "private",
160
+ "hardware": str(runtime_hardware),
161
+ "requested_hardware": str(requested),
162
+ "created_or_reused": True,
163
+ "stage": str(stage),
164
+ "sdk": str(sdk),
165
+ },
166
+ )
167
+ except Exception as exc: # noqa: BLE001
168
+ return _check(
169
+ "P2 private ZeroGPU Space",
170
+ False,
171
+ f"Space inspection failed: {type(exc).__name__}",
172
+ {"space_id": DEV_SPACE_ID},
173
+ )
174
+
175
+
176
+ def check_p3_secrets() -> dict[str, Any]:
177
+ return _check(
178
+ "P3 runtime secret contract",
179
+ _env_present("HF_READ_TOKEN") or _env_present("HF_TOKEN"),
180
+ "read token env var is available for configuring the Space secret"
181
+ if (_env_present("HF_READ_TOKEN") or _env_present("HF_TOKEN"))
182
+ else "HF_READ_TOKEN/HF_TOKEN is not set in this process",
183
+ {"secret_names_only": ["HF_TOKEN"]},
184
+ )
185
+
186
+
187
+ def check_p4_local_versions() -> dict[str, Any]:
188
+ from lightloom.core.config import CONFIG
189
+
190
+ imports = {}
191
+ for module in ("gradio", "huggingface_hub", "transformers", "diffusers", "torch", "llama_cpp"):
192
+ ok, detail = _run_python_import(module)
193
+ imports[module] = {"ok": ok, "detail": detail}
194
+ py_ok = sys.version_info >= (3, 10)
195
+ missing = [name for name, result in imports.items() if not result["ok"]]
196
+ torch_evidence: dict[str, Any] = {}
197
+ try:
198
+ import torch
199
+
200
+ torch_evidence = {
201
+ "torch": getattr(torch, "__version__", "unknown"),
202
+ "cuda_available": bool(torch.cuda.is_available()),
203
+ "cuda_version": getattr(torch.version, "cuda", None),
204
+ "arch_list": list(torch.cuda.get_arch_list()) if torch.cuda.is_available() else [],
205
+ "sm_120_supported": "sm_120" in (
206
+ torch.cuda.get_arch_list() if torch.cuda.is_available() else []
207
+ ),
208
+ "cuda_kernel_smoke": False,
209
+ }
210
+ if torch.cuda.is_available():
211
+ x = torch.ones((8, 8), device="cuda")
212
+ torch_evidence["cuda_kernel_smoke"] = bool((x @ x).sum().item() == 512.0)
213
+ except Exception as exc: # noqa: BLE001
214
+ torch_evidence = {"torch_error": f"{type(exc).__name__}: {exc}"}
215
+ local_cuda_ok = True
216
+ if CONFIG.profile == "local":
217
+ local_cuda_ok = bool(
218
+ torch_evidence.get("cuda_available")
219
+ and torch_evidence.get("sm_120_supported")
220
+ and torch_evidence.get("cuda_kernel_smoke")
221
+ )
222
+ ok = py_ok and not missing and local_cuda_ok
223
+ return _check(
224
+ "P4 local versions",
225
+ ok,
226
+ "Python, required imports, and local CUDA smoke check are available"
227
+ if ok
228
+ else "missing packages or local CUDA/sm_120 smoke check failed",
229
+ {
230
+ "python": sys.version,
231
+ "executable": sys.executable,
232
+ "platform": platform.platform(),
233
+ "imports": imports,
234
+ "torch": torch_evidence,
235
+ "packages_txt_has_ffmpeg": (ROOT / "packages.txt").read_text().strip() == "ffmpeg",
236
+ },
237
+ )
238
+
239
+
240
+ def check_p5_models() -> dict[str, Any]:
241
+ from lightloom.core.config import CONFIG, MODEL_REFS
242
+
243
+ token = _token()
244
+ if token is None:
245
+ return _check(
246
+ "P5 model cards",
247
+ False,
248
+ "HF_WRITE_TOKEN/HF_TOKEN is not set; cannot verify private/gated model cards.",
249
+ {
250
+ "profile": CONFIG.profile,
251
+ "local_flux_dtype": CONFIG.flux_dtype,
252
+ "local_resolution": [CONFIG.width, CONFIG.height],
253
+ "local_aot": CONFIG.flux_aot,
254
+ "model_count": len(MODEL_REFS),
255
+ },
256
+ )
257
+ try:
258
+ from huggingface_hub import HfApi
259
+
260
+ api = HfApi(token=token)
261
+ resolved: dict[str, Any] = {}
262
+ failures: dict[str, str] = {}
263
+ for key, ref in MODEL_REFS.items():
264
+ if key == "rife":
265
+ resolved[key] = {
266
+ "repo_id": ref.repo_id,
267
+ "license": ref.license,
268
+ "note": "GitHub repo; checkpoint license remains VERIFY until selected.",
269
+ }
270
+ continue
271
+ try:
272
+ info = api.model_info(ref.repo_id, token=token)
273
+ license_name = None
274
+ if getattr(info, "card_data", None):
275
+ license_name = getattr(info.card_data, "license", None)
276
+ resolved[key] = {
277
+ "repo_id": ref.repo_id,
278
+ "sha": getattr(info, "sha", None),
279
+ "license": license_name or ref.license,
280
+ "private": bool(getattr(info, "private", False)),
281
+ }
282
+ except Exception as exc: # noqa: BLE001
283
+ failures[key] = f"{type(exc).__name__}: {exc}"
284
+ ok = not failures
285
+ return _check(
286
+ "P5 model cards",
287
+ ok,
288
+ "model cards resolved" if ok else "one or more model cards failed to resolve",
289
+ {
290
+ "profile": CONFIG.profile,
291
+ "local_flux_dtype": CONFIG.flux_dtype,
292
+ "local_resolution": [CONFIG.width, CONFIG.height],
293
+ "local_aot": CONFIG.flux_aot,
294
+ "resolved": resolved,
295
+ "failures": failures,
296
+ },
297
+ )
298
+ except Exception as exc: # noqa: BLE001
299
+ return _check("P5 model cards", False, f"Hub verification failed: {type(exc).__name__}")
300
+
301
+
302
+ def check_p6_disk_cache() -> dict[str, Any]:
303
+ usage = None
304
+ try:
305
+ usage = os.statvfs(ROOT) # type: ignore[attr-defined]
306
+ free_gb = usage.f_bavail * usage.f_frsize / (1024**3)
307
+ except AttributeError:
308
+ import shutil
309
+
310
+ free_gb = shutil.disk_usage(ROOT).free / (1024**3)
311
+ ok = free_gb >= 30
312
+ return _check(
313
+ "P6 disk/cache",
314
+ ok,
315
+ "local disk has room for iterative caches" if ok else "low free disk for model caches",
316
+ {"free_gb": round(free_gb, 2), "estimated_runtime_weights_gb": 20},
317
+ )
318
+
319
+
320
+ def check_p7_spaces_import() -> dict[str, Any]:
321
+ ok, detail = _run_python_import("spaces")
322
+ return _check(
323
+ "P7 spaces decorator",
324
+ ok,
325
+ "spaces package imports" if ok else "spaces package is missing",
326
+ {"detail": detail},
327
+ )
328
+
329
+
330
+ def check_secret_scan() -> dict[str, Any]:
331
+ token_re = __import__("re").compile(r"hf_[A-Za-z0-9]{20,}")
332
+ skip_parts = {".git", ".venv", "__pycache__", ".pytest_cache"}
333
+ skip_suffixes = {".zip", ".7z", ".tar", ".gz", ".png", ".jpg", ".jpeg", ".webp", ".mp4", ".wav"}
334
+ matches: list[str] = []
335
+ try:
336
+ for path in ROOT.rglob("*"):
337
+ if any(part in skip_parts for part in path.parts):
338
+ continue
339
+ if not path.is_file() or path.suffix.lower() in skip_suffixes:
340
+ continue
341
+ try:
342
+ text = path.read_text(encoding="utf-8")
343
+ except UnicodeDecodeError:
344
+ continue
345
+ if token_re.search(text):
346
+ matches.append(str(path.relative_to(ROOT)))
347
+ except Exception as exc: # noqa: BLE001
348
+ return _check("secret scan", False, f"secret scan failed: {type(exc).__name__}")
349
+ ok = not matches
350
+ return _check(
351
+ "secret scan",
352
+ ok,
353
+ "no HF token patterns in tracked files" if ok else "possible HF token pattern found",
354
+ {"match_files": sorted(set(matches))},
355
+ )
356
+
357
+
358
+ def run() -> dict[str, Any]:
359
+ start = time.time()
360
+ RESULTS.mkdir(parents=True, exist_ok=True)
361
+ checks = [
362
+ check_p1_org_membership(),
363
+ check_p2_space(),
364
+ check_p3_secrets(),
365
+ check_p4_local_versions(),
366
+ check_p5_models(),
367
+ check_p6_disk_cache(),
368
+ check_p7_spaces_import(),
369
+ check_secret_scan(),
370
+ ]
371
+ result = {
372
+ "schema_version": 1,
373
+ "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
374
+ "hardware_profile": os.getenv("LIGHTLOOM_PROFILE", "local"),
375
+ "privacy_mode": True,
376
+ "all_green": all(check["ok"] for check in checks),
377
+ "checks": checks,
378
+ "elapsed_s": round(time.time() - start, 3),
379
+ }
380
+ PREFLIGHT_JSON.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
381
+ return result
382
+
383
+
384
+ def main() -> int:
385
+ result = run()
386
+ print("LIGHTLOOM PREFLIGHT")
387
+ for check in result["checks"]:
388
+ status = "PASS" if check["ok"] else "FAIL"
389
+ print(f"{status} {check['name']}: {check['message']}")
390
+ print(f"RESULT {'ALL GREEN' if result['all_green'] else 'BLOCKED'} -> {PREFLIGHT_JSON}")
391
+ return 0 if result["all_green"] else 1
392
+
393
+
394
+ if __name__ == "__main__":
395
+ raise SystemExit(main())
scripts/release_check.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Release gate for Lightloom.
2
+
3
+ This starts strict and becomes stricter through F4/F5. It already enforces the
4
+ privacy addendum's no-secret/no-placeholder posture.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from pathlib import Path
11
+ import re
12
+ import subprocess
13
+ import sys
14
+
15
+ ROOT = Path(__file__).resolve().parents[1]
16
+ TOKEN_RE = re.compile(r"hf_[A-Za-z0-9]{20,}")
17
+
18
+
19
+ def fail(message: str) -> str:
20
+ return f"FAIL {message}"
21
+
22
+
23
+ def ok(message: str) -> str:
24
+ return f"PASS {message}"
25
+
26
+
27
+ def secret_scan() -> tuple[bool, str]:
28
+ tracked = subprocess.run(
29
+ ["git", "ls-files"],
30
+ cwd=ROOT,
31
+ text=True,
32
+ capture_output=True,
33
+ check=False,
34
+ )
35
+ if tracked.returncode != 0:
36
+ return False, "git ls-files failed"
37
+ offenders: list[str] = []
38
+ for rel in tracked.stdout.splitlines():
39
+ path = ROOT / rel
40
+ if not path.is_file():
41
+ continue
42
+ try:
43
+ text = path.read_text(encoding="utf-8")
44
+ except UnicodeDecodeError:
45
+ continue
46
+ if TOKEN_RE.search(text):
47
+ offenders.append(rel)
48
+ if offenders:
49
+ return False, "token-like secret found in tracked files: " + ", ".join(offenders)
50
+ return True, "no token-like secrets in tracked files"
51
+
52
+
53
+ def preflight_result() -> tuple[bool, str]:
54
+ path = ROOT / "benchmarks" / "results" / "preflight.json"
55
+ if not path.exists():
56
+ return False, "benchmarks/results/preflight.json is missing"
57
+ data = json.loads(path.read_text(encoding="utf-8"))
58
+ if not data.get("all_green"):
59
+ return False, "preflight is not ALL GREEN"
60
+ return True, "preflight ALL GREEN"
61
+
62
+
63
+ def h6_placeholder_gate() -> tuple[bool, str]:
64
+ readme = ROOT / "README.md"
65
+ if not readme.exists():
66
+ return False, "README.md is missing"
67
+ text = readme.read_text(encoding="utf-8")
68
+ release_mode = (ROOT / "docs" / "submission" / "H6_GREEN").exists()
69
+ if release_mode and "⟦PLACEHOLDER⟧" in text:
70
+ return False, "H6 release mode forbids README placeholders"
71
+ return True, "README placeholder state accepted for current private mode"
72
+
73
+
74
+ def main() -> int:
75
+ checks = [secret_scan(), preflight_result(), h6_placeholder_gate()]
76
+ for passed, message in checks:
77
+ print(ok(message) if passed else fail(message))
78
+ all_green = all(passed for passed, _ in checks)
79
+ print("RESULT " + ("ALL GREEN" if all_green else "BLOCKED"))
80
+ return 0 if all_green else 1
81
+
82
+
83
+ if __name__ == "__main__":
84
+ raise SystemExit(main())
scripts/set_hf_env.ps1 ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ param(
2
+ [switch]$PersistUser = $true,
3
+ [string]$DevSpaceId = ""
4
+ )
5
+
6
+ $ErrorActionPreference = "Stop"
7
+
8
+ function Read-SecretPlainText {
9
+ param([Parameter(Mandatory = $true)][string]$Name)
10
+ $secure = Read-Host -Prompt "Enter $Name" -AsSecureString
11
+ $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
12
+ try {
13
+ return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
14
+ }
15
+ finally {
16
+ if ($bstr -ne [IntPtr]::Zero) {
17
+ [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
18
+ }
19
+ }
20
+ }
21
+
22
+ function Set-EnvValue {
23
+ param(
24
+ [Parameter(Mandatory = $true)][string]$Name,
25
+ [Parameter(Mandatory = $true)][string]$Value
26
+ )
27
+ Set-Item -Path "Env:$Name" -Value $Value
28
+ if ($PersistUser) {
29
+ [Environment]::SetEnvironmentVariable($Name, $Value, "User")
30
+ }
31
+ }
32
+
33
+ $writeToken = Read-SecretPlainText -Name "HF_WRITE_TOKEN"
34
+ $readToken = Read-SecretPlainText -Name "HF_READ_TOKEN"
35
+
36
+ if (-not $writeToken.StartsWith("hf_")) {
37
+ throw "HF_WRITE_TOKEN must look like a Hugging Face token."
38
+ }
39
+ if (-not $readToken.StartsWith("hf_")) {
40
+ throw "HF_READ_TOKEN must look like a Hugging Face token."
41
+ }
42
+
43
+ Set-EnvValue -Name "HF_WRITE_TOKEN" -Value $writeToken
44
+ Set-EnvValue -Name "HF_READ_TOKEN" -Value $readToken
45
+ Set-EnvValue -Name "HF_TOKEN" -Value $readToken
46
+
47
+ if ($DevSpaceId.Trim().Length -gt 0) {
48
+ Set-EnvValue -Name "LIGHTLOOM_DEV_SPACE_ID" -Value $DevSpaceId.Trim()
49
+ }
50
+
51
+ Set-EnvValue -Name "LIGHTLOOM_PROFILE" -Value "local"
52
+ Set-EnvValue -Name "LIGHTLOOM_HF_ORG" -Value "build-small-hackathon"
53
+
54
+ Write-Host "HF environment variables installed. Token values were not printed."
55
+ Write-Host "Restart the terminal or launch Codex from this shell for persisted user env changes to apply."
src/lightloom/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Lightloom package."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
src/lightloom/compliance/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Compliance helpers for parameter and off-grid checks."""
src/lightloom/compliance/params_ledger.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Runtime parameter ledger.
2
+
3
+ Parameter contribution: 0B. This module enforces the <=32B total runtime
4
+ parameter rule and separates runtime models from build-time assets.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ import sys
12
+ from typing import Iterable
13
+
14
+ if __package__ in {None, ""}:
15
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
16
+
17
+ from lightloom.core.config import MODEL_REFS
18
+
19
+ PARAMETER_LIMIT = 32_000_000_000
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class LedgerEntry:
24
+ name: str
25
+ role: str
26
+ repo_id: str
27
+ params: int
28
+ license: str
29
+ runtime: bool
30
+ revision: str | None = None
31
+
32
+
33
+ def entries() -> list[LedgerEntry]:
34
+ return [
35
+ LedgerEntry(
36
+ name=name,
37
+ role=ref.role,
38
+ repo_id=ref.repo_id,
39
+ params=ref.params,
40
+ license=ref.license,
41
+ runtime=ref.runtime,
42
+ revision=ref.revision,
43
+ )
44
+ for name, ref in MODEL_REFS.items()
45
+ ]
46
+
47
+
48
+ def runtime_entries() -> list[LedgerEntry]:
49
+ return [entry for entry in entries() if entry.runtime]
50
+
51
+
52
+ def total_runtime_params(items: Iterable[LedgerEntry] | None = None) -> int:
53
+ selected = list(items) if items is not None else runtime_entries()
54
+ return sum(entry.params for entry in selected if entry.runtime)
55
+
56
+
57
+ def assert_under_limit() -> None:
58
+ total = total_runtime_params()
59
+ if total > PARAMETER_LIMIT:
60
+ raise AssertionError(f"runtime params exceed limit: {total} > {PARAMETER_LIMIT}")
61
+
62
+
63
+ def markdown_table() -> str:
64
+ rows = [
65
+ "| Name | Role | Repo | Params (B) | License | Runtime |",
66
+ "|---|---|---|---:|---|---|",
67
+ ]
68
+ for entry in entries():
69
+ rows.append(
70
+ "| {name} | {role} | `{repo}` | {params:.3f} | {license} | {runtime} |".format(
71
+ name=entry.name,
72
+ role=entry.role,
73
+ repo=entry.repo_id,
74
+ params=entry.params / 1_000_000_000,
75
+ license=entry.license,
76
+ runtime="yes" if entry.runtime else "build-time",
77
+ )
78
+ )
79
+ rows.append(f"| **TOTAL runtime** | | | **{total_runtime_params() / 1_000_000_000:.3f}** | | |")
80
+ return "\n".join(rows)
81
+
82
+
83
+ if __name__ == "__main__":
84
+ assert_under_limit()
85
+ print(markdown_table())
src/lightloom/core/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Core runtime configuration and session primitives."""
src/lightloom/core/config.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Runtime/build-time configuration for Lightloom.
2
+
3
+ Parameter contribution: 0B. This module only declares model ids, revisions,
4
+ profiles, and runtime knobs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ import os
11
+ from typing import Literal
12
+
13
+ Profile = Literal["local", "space"]
14
+
15
+
16
+ def _profile_from_env() -> Profile:
17
+ raw = os.getenv("LIGHTLOOM_PROFILE", "local").strip().lower()
18
+ if raw not in {"local", "space"}:
19
+ raise ValueError("LIGHTLOOM_PROFILE must be 'local' or 'space'")
20
+ return raw # type: ignore[return-value]
21
+
22
+
23
+ LIGHTLOOM_PROFILE: Profile = _profile_from_env()
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class ModelRef:
28
+ role: str
29
+ repo_id: str
30
+ revision: str | None
31
+ license: str
32
+ params: int
33
+ runtime: bool = True
34
+ notes: str = ""
35
+ filename: str | None = None
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class RuntimeConfig:
40
+ profile: Profile
41
+ width: int
42
+ height: int
43
+ flux_dtype: Literal["fp8", "bf16"]
44
+ flux_aot: bool
45
+ flux_steps: int
46
+ guidance_scale: float
47
+ session_gpu_budget_s: float
48
+ showcase_only: bool
49
+ zero_gpu_duration_s: int
50
+
51
+
52
+ LOCAL_CONFIG = RuntimeConfig(
53
+ profile="local",
54
+ width=768,
55
+ height=432,
56
+ flux_dtype="fp8",
57
+ flux_aot=False,
58
+ flux_steps=4,
59
+ guidance_scale=1.0,
60
+ session_gpu_budget_s=90.0,
61
+ showcase_only=os.getenv("LIGHTLOOM_SHOWCASE_ONLY", "0") == "1",
62
+ zero_gpu_duration_s=45,
63
+ )
64
+
65
+ SPACE_CONFIG = RuntimeConfig(
66
+ profile="space",
67
+ width=int(os.getenv("LIGHTLOOM_WIDTH", "1024")),
68
+ height=int(os.getenv("LIGHTLOOM_HEIGHT", "576")),
69
+ flux_dtype=os.getenv("LIGHTLOOM_FLUX_DTYPE", "fp8").lower(), # type: ignore[arg-type]
70
+ flux_aot=os.getenv("LIGHTLOOM_FLUX_AOT", "1") == "1",
71
+ flux_steps=int(os.getenv("LIGHTLOOM_FLUX_STEPS", "4")),
72
+ guidance_scale=float(os.getenv("LIGHTLOOM_GUIDANCE_SCALE", "1.0")),
73
+ session_gpu_budget_s=float(os.getenv("LIGHTLOOM_SESSION_GPU_BUDGET", "90")),
74
+ showcase_only=os.getenv("LIGHTLOOM_SHOWCASE_ONLY", "0") == "1",
75
+ zero_gpu_duration_s=int(os.getenv("LIGHTLOOM_ZERO_GPU_DURATION", "45")),
76
+ )
77
+
78
+ CONFIG = LOCAL_CONFIG if LIGHTLOOM_PROFILE == "local" else SPACE_CONFIG
79
+
80
+ # Candidate model refs. Revisions are intentionally unresolved until preflight
81
+ # P5 records exact Hub revisions into benchmarks/results/preflight.json.
82
+ MODEL_REFS: dict[str, ModelRef] = {
83
+ "asr": ModelRef(
84
+ role="ASR",
85
+ repo_id="CohereLabs/cohere-transcribe-03-2026",
86
+ revision="b1eacc2686a3d08ceaae5f24a88b1d519620bc09",
87
+ license="apache-2.0",
88
+ params=2_000_000_000,
89
+ ),
90
+ "translator": ModelRef(
91
+ role="Translator",
92
+ repo_id="CohereLabs/tiny-aya-global-GGUF",
93
+ revision="a602ea7eeec3a4ad6f77a1b8cf6a53512824922b",
94
+ license="cc-by-nc-4.0",
95
+ params=3_350_000_000,
96
+ filename="*Q4_K_M*.gguf",
97
+ ),
98
+ "director": ModelRef(
99
+ role="Director",
100
+ repo_id="openbmb/MiniCPM5-1B",
101
+ revision="4e9de7a0778dc1c362e983e6858f0e77542cbdca",
102
+ license="apache-2.0",
103
+ params=1_000_000_000,
104
+ notes="GGUF repo id remains VERIFY item V1.",
105
+ ),
106
+ "painter": ModelRef(
107
+ role="Painter",
108
+ repo_id="black-forest-labs/FLUX.2-klein-4B",
109
+ revision="e7b7dc27f91deacad38e78976d1f2b499d76a294",
110
+ license="apache-2.0",
111
+ params=4_000_000_000,
112
+ notes="Local profile requires FP8 because 12GB VRAM cannot fit BF16.",
113
+ ),
114
+ "painter_base_train": ModelRef(
115
+ role="Painter training base",
116
+ repo_id="black-forest-labs/FLUX.2-klein-base-4B",
117
+ revision="a3b4f4849157f664bdbc776fd7453c2783562f4d",
118
+ license="apache-2.0",
119
+ params=4_000_000_000,
120
+ runtime=False,
121
+ ),
122
+ "depth": ModelRef(
123
+ role="Depth",
124
+ repo_id="depth-anything/Depth-Anything-V2-Small-hf",
125
+ revision="5426e4f0f36572d16453bbda7a8389317b1bef99",
126
+ license="apache-2.0",
127
+ params=25_000_000,
128
+ ),
129
+ "sound": ModelRef(
130
+ role="Ambient audio",
131
+ repo_id="stabilityai/stable-audio-open-small",
132
+ revision="dc620d91535857b72ebb59b4ca45978db6d417f5",
133
+ license="other",
134
+ params=341_000_000,
135
+ notes="Model card license field is 'other'; card text declares Stability AI Community License.",
136
+ ),
137
+ "rife": ModelRef(
138
+ role="Export interpolation",
139
+ repo_id="hzwer/Practical-RIFE",
140
+ revision=None,
141
+ license="VERIFY",
142
+ params=10_000_000,
143
+ ),
144
+ "vad": ModelRef(
145
+ role="Voice activity detection",
146
+ repo_id="onnx-community/silero-vad",
147
+ revision="e71cae966052b992a7eca6b17738916ce0eca4ec",
148
+ license="mit",
149
+ params=2_000_000,
150
+ ),
151
+ }
152
+
153
+ MUST_SET = (
154
+ "continuity engine",
155
+ "2.5D parallax",
156
+ "slate",
157
+ "Recital mode",
158
+ "Showcase",
159
+ "quota guard",
160
+ )
161
+
162
+ F6_CUT_ORDER = ("RIFE", "ambient audio", "Aya", "LoRA")
tests/test_compliance.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ import re
5
+
6
+ ROOT = Path(__file__).resolve().parents[1]
7
+ RUNTIME_DIRS = [ROOT / "src", ROOT / "frontend", ROOT / "app.py"]
8
+
9
+ FORBIDDEN = [
10
+ "api.openai.com",
11
+ "api.cohere",
12
+ "bfl.ai/api",
13
+ "InferenceClient",
14
+ "huggingface_hub.InferenceApi",
15
+ ]
16
+
17
+ TOKEN_RE = re.compile(r"hf_[A-Za-z0-9]{20,}")
18
+
19
+
20
+ def _runtime_files() -> list[Path]:
21
+ files: list[Path] = []
22
+ for root in RUNTIME_DIRS:
23
+ if root.is_file():
24
+ files.append(root)
25
+ elif root.exists():
26
+ files.extend(path for path in root.rglob("*") if path.is_file())
27
+ return files
28
+
29
+
30
+ def test_no_cloud_api_literals_in_runtime() -> None:
31
+ offenders: list[str] = []
32
+ for path in _runtime_files():
33
+ text = path.read_text(encoding="utf-8", errors="ignore")
34
+ for pattern in FORBIDDEN:
35
+ if pattern in text:
36
+ offenders.append(f"{path.relative_to(ROOT)}:{pattern}")
37
+ assert offenders == []
38
+
39
+
40
+ def test_no_hf_tokens_in_repo_text_files() -> None:
41
+ offenders: list[str] = []
42
+ for path in ROOT.rglob("*"):
43
+ if ".git" in path.parts or ".venv" in path.parts:
44
+ continue
45
+ if not path.is_file():
46
+ continue
47
+ try:
48
+ text = path.read_text(encoding="utf-8")
49
+ except UnicodeDecodeError:
50
+ continue
51
+ if TOKEN_RE.search(text):
52
+ offenders.append(str(path.relative_to(ROOT)))
53
+ assert offenders == []
tests/test_gate_g1.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from benchmarks.gate_g1 import DEFAULT_JSON, run
7
+
8
+
9
+ def test_g1_dry_run_writes_non_authoritative_results() -> None:
10
+ data = run(dry_run=True, reps=2, allow_local=False)
11
+ assert data["authoritative"] is False
12
+ assert data["decision"]["status"] == "blocked"
13
+ assert DEFAULT_JSON.exists()
14
+ saved = json.loads(Path(DEFAULT_JSON).read_text(encoding="utf-8"))
15
+ assert saved["results"]
16
+ assert all(item["status"] == "dry_run" for item in saved["results"])
tests/test_ledger.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from lightloom.compliance.params_ledger import PARAMETER_LIMIT, runtime_entries, total_runtime_params
2
+ from lightloom.core.config import CONFIG
3
+
4
+
5
+ def test_runtime_params_under_32b() -> None:
6
+ assert total_runtime_params() <= PARAMETER_LIMIT
7
+
8
+
9
+ def test_runtime_entries_are_positive_and_licensed() -> None:
10
+ for entry in runtime_entries():
11
+ assert entry.params > 0
12
+ assert entry.license
13
+
14
+
15
+ def test_local_profile_is_5070_safe() -> None:
16
+ if CONFIG.profile == "local":
17
+ assert CONFIG.flux_dtype == "fp8"
18
+ assert (CONFIG.width, CONFIG.height) == (768, 432)
19
+ assert CONFIG.flux_aot is False