chore: deploy private lightloom build
Browse files- .gitignore +50 -0
- AGENTS.md +89 -0
- CLAUDE.md +30 -0
- MASTER_PLAN.md +85 -0
- README.md +10 -8
- app.py +85 -0
- benchmarks/__init__.py +1 -0
- benchmarks/gate_g1.py +301 -0
- benchmarks/results/.gitkeep +1 -0
- benchmarks/results/preflight.json +191 -0
- docs/01_ARCHITECTURE.md +176 -0
- docs/02_MODELS_AND_MATH.md +93 -0
- docs/03_PROMPTS.md +154 -0
- docs/04_UI_SPEC.md +115 -0
- docs/05_EXECUTION_PLAN.md +67 -0
- docs/06_TESTING_QA.md +66 -0
- docs/07_RISKS.md +24 -0
- docs/08_COMPLIANCE_HACKATHON.md +101 -0
- docs/VERIFY_LOG.md +13 -0
- packages.txt +1 -0
- pyproject.toml +21 -0
- requirements.txt +14 -0
- scripts/deploy_private_space.py +93 -0
- scripts/fetch_gate_g1.py +53 -0
- scripts/preflight.py +395 -0
- scripts/release_check.py +84 -0
- scripts/set_hf_env.ps1 +55 -0
- src/lightloom/__init__.py +5 -0
- src/lightloom/compliance/__init__.py +1 -0
- src/lightloom/compliance/params_ledger.py +85 -0
- src/lightloom/core/__init__.py +1 -0
- src/lightloom/core/config.py +162 -0
- tests/test_compliance.py +53 -0
- tests/test_gate_g1.py +16 -0
- tests/test_ledger.py +19 -0
.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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|