macayaven commited on
Commit
309b545
·
verified ·
1 Parent(s): 7384285

Deploy clean Small Cuts fallback Space

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .claude/launch.json +16 -0
  2. .claude/scheduled_tasks.lock +1 -0
  3. .claude/settings.json +23 -0
  4. .claude/skills/deploy-space/SKILL.md +48 -0
  5. .gitattributes +10 -0
  6. .github/workflows/ci.yml +40 -0
  7. .github/workflows/eval-on-spark.yml +64 -0
  8. .github/workflows/fetch-eval-photos.yml +33 -0
  9. .gitignore +29 -0
  10. .hfignore +13 -0
  11. CLAUDE.md +97 -0
  12. README.md +133 -7
  13. app.py +60 -0
  14. docs/architecture.md +72 -0
  15. docs/contracts/CLAUDE.md +25 -0
  16. docs/contracts/README.md +84 -0
  17. docs/contracts/control.schema.json +46 -0
  18. docs/contracts/moment.schema.json +152 -0
  19. docs/contracts/narrated-scene.schema.json +148 -0
  20. docs/contracts/scene-audio.schema.json +35 -0
  21. docs/eval/prompt-ab-comparison.md +35 -0
  22. docs/eval/run-002-gemma-baseline.md +99 -0
  23. docs/eval/run-004-scored.md +76 -0
  24. docs/eval/run-005-scored.md +107 -0
  25. docs/eval/run-006-scored.md +107 -0
  26. docs/field-notes-small-cuts.md +97 -0
  27. docs/hackathon-rules.md +111 -0
  28. docs/implementation-plan.md +53 -0
  29. docs/product-strategy.md +124 -0
  30. docs/product/architecture.md +152 -0
  31. docs/product/rehearsal-2026-06-12.md +47 -0
  32. docs/progress.md +36 -0
  33. docs/regular-pipeline-library-2026-06-15.md +78 -0
  34. docs/setup.md +92 -0
  35. docs/specs/llama-backend.md +72 -0
  36. docs/specs/theme.md +63 -0
  37. docs/specs/title-card-wiring.md +51 -0
  38. docs/specs/title-card.md +60 -0
  39. docs/specs/tts.md +78 -0
  40. docs/specs/video-input.md +77 -0
  41. kb/2026-06-11-directors-cut-peer-synthesis-for-small-cuts.md +108 -0
  42. kb/2026-06-11-small-cuts-architecture-decision.md +27 -0
  43. kb/2026-06-11-small-cuts-hackathon-grounding.md +31 -0
  44. kb/2026-06-11-small-cuts-product-strategy.md +30 -0
  45. kb/2026-06-11-small-cuts-repo-bootstrap.md +26 -0
  46. kb/README.md +14 -0
  47. pyproject.toml +61 -0
  48. requirements.txt +7 -0
  49. scripts/dress_rehearsal.py +475 -0
  50. scripts/hooks/block-secrets.sh +17 -0
.claude/launch.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "0.0.1",
3
+ "configurations": [
4
+ {
5
+ "name": "small-cuts-mock",
6
+ "runtimeExecutable": "uv",
7
+ "runtimeArgs": ["run", "python", "app.py"],
8
+ "env": {
9
+ "SMALL_CUTS_BACKEND": "mock",
10
+ "SMALL_CUTS_TTS_BACKEND": "mock",
11
+ "GRADIO_SERVER_PORT": "7861"
12
+ },
13
+ "port": 7861
14
+ }
15
+ ]
16
+ }
.claude/scheduled_tasks.lock ADDED
@@ -0,0 +1 @@
 
 
1
+ {"sessionId":"7d2c60ca-a075-4ecc-b94d-738f0cf355cc","pid":9019,"procStart":"Sat Jun 13 22:58:53 2026","acquiredAt":1781392518293}
.claude/settings.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "permissions": {
3
+ "defaultMode": "acceptEdits",
4
+ "allow": [
5
+ "Bash",
6
+ "Write",
7
+ "Edit",
8
+ "WebFetch"
9
+ ]
10
+ },
11
+ "hooks": {
12
+ "SessionStart": [
13
+ {
14
+ "hooks": [
15
+ {
16
+ "type": "command",
17
+ "command": "bash scripts/tailnet-connect.sh || true"
18
+ }
19
+ ]
20
+ }
21
+ ]
22
+ }
23
+ }
.claude/skills/deploy-space/SKILL.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: deploy-space
3
+ description: Deploy the Small Cuts app to the Hugging Face Space (build-small-hackathon/small-cuts) and smoke-test it. Use when shipping a new build to the Space.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Deploy Small Cuts to the HF Space
8
+
9
+ Ship the current build to `build-small-hackathon/small-cuts` and verify it. The Space is the judged
10
+ artifact — never push red.
11
+
12
+ ## 1. Green gate first (must mirror CI)
13
+ ```bash
14
+ uv run ruff check . && uv run ruff format --check . && uv run pytest
15
+ ```
16
+ Stop if anything fails.
17
+
18
+ ## 2. Auth
19
+ `hf auth whoami` must show a write-scoped login. If not, ask the user to run `! hf auth login`.
20
+ (For exact `hf` CLI syntax, use the `huggingface-skills:hf-cli` skill.)
21
+
22
+ ## 3. Upload what the Space needs
23
+ `app.py`, `src/` (including `src/small_cuts/seed_media/` — the seed clips are static files served via
24
+ `gr.set_static_paths`), `requirements.txt`, and the Space `README.md` (its frontmatter carries the
25
+ `track:` / `achievement:` / `sponsor:` tags). Either:
26
+ ```bash
27
+ hf upload --repo-type space build-small-hackathon/small-cuts . . \
28
+ --include "app.py" --include "requirements.txt" --include "README.md" --include "src/**"
29
+ ```
30
+ or push to the Space's git remote.
31
+
32
+ ## 4. Space variables (backends)
33
+ ```bash
34
+ hf spaces variables build-small-hackathon/small-cuts # list current
35
+ # real models on the Space: SMALL_CUTS_BACKEND=transformers SMALL_CUTS_TTS_BACKEND=kokoro
36
+ ```
37
+
38
+ ## 5. Smoke test (the fork-poison detector)
39
+ After the Space restarts, run a `gradio_client` smoke: **narrate → TTS → narrate-again** — the third
40
+ call is what catches kokoro fork-poisoning. Tail logs:
41
+ ```bash
42
+ hf spaces logs build-small-hackathon/small-cuts --tail
43
+ ```
44
+
45
+ ## Reminders
46
+ - ZeroGPU: `@spaces.GPU` marks must be on the handlers Gradio binds; no torch in the main process
47
+ (see `src/small_cuts/CLAUDE.md`).
48
+ - Confirm `seed_media/*.mp4` actually shipped — the hero library is empty without them.
.gitattributes CHANGED
@@ -33,3 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ src/small_cuts/seed_media/desk-laptop.mp3 filter=lfs diff=lfs merge=lfs -text
37
+ src/small_cuts/seed_media/desk-laptop.mp4 filter=lfs diff=lfs merge=lfs -text
38
+ src/small_cuts/seed_media/night-drive.mp3 filter=lfs diff=lfs merge=lfs -text
39
+ src/small_cuts/seed_media/night-drive.mp4 filter=lfs diff=lfs merge=lfs -text
40
+ src/small_cuts/seed_media/rayuela.mp3 filter=lfs diff=lfs merge=lfs -text
41
+ src/small_cuts/seed_media/rayuela.mp4 filter=lfs diff=lfs merge=lfs -text
42
+ src/small_cuts/seed_media/street-parked-car.mp3 filter=lfs diff=lfs merge=lfs -text
43
+ src/small_cuts/seed_media/street-parked-car.mp4 filter=lfs diff=lfs merge=lfs -text
44
+ src/small_cuts/seed_media/the-stumble.mp3 filter=lfs diff=lfs merge=lfs -text
45
+ src/small_cuts/seed_media/the-stumble.mp4 filter=lfs diff=lfs merge=lfs -text
.github/workflows/ci.yml ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+ pull-requests: read
12
+
13
+ jobs:
14
+ ci:
15
+ name: ci
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0
21
+
22
+ - uses: astral-sh/setup-uv@v5
23
+ with:
24
+ python-version: "3.11"
25
+
26
+ - name: Install
27
+ run: uv sync --extra dev
28
+
29
+ - name: Lint
30
+ run: |
31
+ uv run ruff check .
32
+ uv run ruff format --check .
33
+
34
+ - name: Tests
35
+ run: uv run pytest
36
+
37
+ - name: Secret scan (gitleaks)
38
+ uses: gitleaks/gitleaks-action@v2
39
+ env:
40
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
.github/workflows/eval-on-spark.yml ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: eval-on-spark
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ images_dir:
7
+ description: Directory of eval photos on the Spark
8
+ default: ~/eval-photos
9
+ models:
10
+ description: Space-separated model ids
11
+ default: "google/gemma-3-4b-it Qwen/Qwen2.5-VL-3B-Instruct"
12
+ # Cloud sessions can't dispatch workflows (403) but can push — pushing an
13
+ # eval/** branch runs the eval with the defaults below.
14
+ push:
15
+ branches: ["eval/**"]
16
+
17
+ jobs:
18
+ eval:
19
+ runs-on: [self-hosted, spark]
20
+ timeout-minutes: 240
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - name: GPU + environment probe
25
+ run: |
26
+ uname -a
27
+ nvidia-smi || echo "WARN: nvidia-smi unavailable"
28
+
29
+ - name: Ensure uv
30
+ run: |
31
+ command -v uv >/dev/null || curl -LsSf https://astral.sh/uv/install.sh | sh
32
+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
33
+
34
+ - name: Install
35
+ run: uv sync --extra local
36
+
37
+ - name: Run eval (one process per model for crash isolation)
38
+ run: |
39
+ IMAGES_RAW="${{ inputs.images_dir || '~/eval-photos' }}"
40
+ IMAGES="${IMAGES_RAW/#\~/$HOME}"
41
+ MODELS="${{ inputs.models || 'google/gemma-3-4b-it Qwen/Qwen2.5-VL-3B-Instruct' }}"
42
+ ls -la "$IMAGES" || echo "Directory not found: $IMAGES"
43
+ rm -f eval-report.md
44
+ for M in $MODELS; do
45
+ SLUG="$(echo "$M" | tr '/' '_')"
46
+ echo "::group::eval $M"
47
+ # Each model runs in its own process: an OOM/segfault kills only
48
+ # this iteration, not the whole eval.
49
+ if uv run python -m small_cuts.eval --images "$IMAGES" \
50
+ --models "$M" --out "eval-$SLUG.md"; then
51
+ cat "eval-$SLUG.md" >> eval-report.md
52
+ else
53
+ printf '\n## %s — CRASHED (non-Python failure, see logs)\n' "$M" >> eval-report.md
54
+ fi
55
+ echo "::endgroup::"
56
+ done
57
+ cat eval-report.md >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || true
58
+
59
+ - name: Upload report
60
+ if: always()
61
+ uses: actions/upload-artifact@v4
62
+ with:
63
+ name: eval-report
64
+ path: eval-report.md
.github/workflows/fetch-eval-photos.yml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: fetch-eval-photos
2
+
3
+ # Dispatch-only relay: uploads the Spark's eval photos as a short-lived
4
+ # artifact so tailnet-less sessions (cloud, or local without SSH grants)
5
+ # can score the eval report against the actual images.
6
+ on:
7
+ workflow_dispatch:
8
+ inputs:
9
+ images_dir:
10
+ description: Directory of eval photos on the Spark
11
+ default: ~/eval-photos
12
+
13
+ jobs:
14
+ upload:
15
+ runs-on: [self-hosted, spark]
16
+ timeout-minutes: 10
17
+ steps:
18
+ - name: Stage photos
19
+ env:
20
+ SRC_RAW: ${{ inputs.images_dir || '~/eval-photos' }}
21
+ run: |
22
+ SRC="${SRC_RAW/#\~/$HOME}"
23
+ rm -rf staged-photos && mkdir staged-photos
24
+ cp "$SRC"/* staged-photos/
25
+ ls -la staged-photos
26
+
27
+ - name: Upload photos artifact
28
+ uses: actions/upload-artifact@v4
29
+ with:
30
+ name: eval-photos
31
+ path: staged-photos/
32
+ if-no-files-found: error
33
+ retention-days: 1
.gitignore ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ dist/
7
+ build/
8
+
9
+ # Tooling
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ uv.lock
13
+
14
+ # Models / large artifacts
15
+ *.gguf
16
+ models/
17
+ flagged/
18
+
19
+ # OS / editor
20
+ .DS_Store
21
+ .idea/
22
+ .vscode/
23
+
24
+ # Env — never commit secrets
25
+ .env
26
+ .env.*
27
+ .playwright-mcp/
28
+ ui-proposal.jpeg
29
+ aimode-content.yml
.hfignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git/
2
+ .venv/
3
+ ios/
4
+ docs/reviews/
5
+ docs/superpowers/
6
+ docs/submission-readiness-*.md
7
+ docs/demo-readiness.md
8
+ docs/submission-assets-2026-06-15.md
9
+ __pycache__/
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .playwright-mcp/
13
+ xcodebuild-*.log
CLAUDE.md ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md — Small Cuts (root, always-on)
2
+
3
+ **Small Cuts** turns first-person moments into grounded, cinematic, *spoken* narration using only
4
+ **small (≤32B) open models running off-grid**. Goal (single, dual-purpose): **win/place in the
5
+ Build Small Hackathon AND/OR make this a portfolio-grade, door-opening applied-AI project.** It is
6
+ the strategic successor to *Director's Cut*. Full essence, lineage, and the canonical architecture
7
+ live in the **KB** — see Documentation map.
8
+
9
+ > **Deadline: 2026-06-15 23:59 UTC.** Branch-of-record: `claude/adoring-clarke-49l3uk` → protected `main`.
10
+
11
+ ---
12
+
13
+ ## Documentation map (READ FIRST — anti-divergence policy)
14
+ **One home per fact. Link, never copy.**
15
+ - **Operational / as-built** (commands, env vars, conventions, contracts-as-enforced) → **these
16
+ CLAUDE.md files + `docs/`**, versioned with the code.
17
+ - **Strategy, lineage, decisions, the architecture, coordination** → **the KB**
18
+ (`knowledge-base` MCP → `knowledge_read`/`knowledge_search`; notes under
19
+ `10-projects/small-cuts/`). The architecture's **single source of truth** is the interactive
20
+ artifact `10-projects/small-cuts/architecture/index.html`.
21
+ - The **canonical command list lives once here** (below). Subproject CLAUDE.md files add only
22
+ module-local specifics and **reference** this — they never restate it. (We keep the shared list
23
+ in this always-loaded root rather than a separate `@import`-ed snippet so no extra repo file is
24
+ introduced; `@import` is the mechanism if a dedicated snippet file is added later.)
25
+ - **Claude Code auto-memory is a third writer**: operational learnings may land there, but
26
+ **architecture/strategy stays in the KB**.
27
+ - **Nested CLAUDE.md are lazy-loaded** (only when you open a file in that subdir):
28
+ `src/small_cuts/` (space) · `src/small_cuts/engine/` (inference) · `ios/SmallCuts/` (mobile) ·
29
+ `docs/contracts/` (contracts).
30
+
31
+ ## Repo map
32
+ - `app.py` — HF Space entrypoint (Gradio, ZeroGPU). `requirements.txt` — Space runtime deps.
33
+ - `src/small_cuts/` — Python product: `narrator.py` (VLM backends), `tts.py` (Kokoro), `styles.py`
34
+ (6 director styles + grounded prompt v3), `title_card.py`, `theme.py` (Off-Brand), `ui.py`,
35
+ `viewer.py` (streaming viewer; engine + upload modes), `frames.py` (PyAV), `eval.py`.
36
+ - `src/small_cuts/engine/` — real-time home-node engine (FastAPI WS `/v1/session`, `library.py`
37
+ sqlite+media, SSE stream). Launch: `python -m small_cuts.engine`.
38
+ - `ios/SmallCuts/` — Meta-glasses capture app (XcodeGen, DAT 0.7). See its `RUNBOOK.md`.
39
+ - `docs/` — `contracts/` (v1.1.0 schemas), `product/architecture.md` (D1–D9 + the loop),
40
+ `architecture.md`, `product-strategy.md`, `hackathon-rules.md`, `eval/`, `progress.md`, `specs/`.
41
+ - `tests/` — pytest incl. golden-sample contract tests. `kb/` — legacy mirror notes (canonical KB
42
+ is the `knowledge-base` MCP).
43
+
44
+ ## Canonical commands (author-once; subprojects reference these)
45
+ ```bash
46
+ # install — CI-equivalent minimal:
47
+ uv sync --extra dev
48
+ # install — full local (engine + TTS + transformers):
49
+ uv sync --extra dev --extra engine --extra tts --extra local
50
+
51
+ # local gate — MUST mirror CI exactly (all three, in order):
52
+ uv run ruff check . && uv run ruff format --check . && uv run pytest
53
+
54
+ # run the Space/viewer locally (bare `uv run` prunes the tts extra — keep --no-sync):
55
+ uv run --no-sync python app.py
56
+ # serve on the tailnet:
57
+ GRADIO_SERVER_NAME=0.0.0.0 GRADIO_SERVER_PORT=7861 uv run --no-sync python app.py # http://mac-studio:7861
58
+
59
+ # run the real-time engine (port 8077; needs `brew install llama.cpp` for llama-server):
60
+ SMALL_CUTS_BACKEND=llama_cpp SMALL_CUTS_TTS_BACKEND=kokoro uv run python -m small_cuts.engine
61
+ SMALL_CUTS_BACKEND=mock uv run python -m small_cuts.engine # no-model smoke
62
+
63
+ # iOS (see ios/SmallCuts/CLAUDE.md + RUNBOOK.md):
64
+ DEVELOPER_DIR=/Volumes/mac-studio-ssd/Applications/Xcode.app/Contents/Developer xcodegen
65
+
66
+ # branch-of-record sync AFTER every squash-merge into main:
67
+ git fetch && git merge -s ours origin/main && git push
68
+ gh pr merge --auto --squash
69
+ ```
70
+
71
+ ## Conventions
72
+ - **Local gate must mirror CI**: `ruff check` **and** `ruff format --check` **and** `pytest`
73
+ (CI also runs gitleaks). Never trust a piped `gh pr checks --watch` exit code.
74
+ - **Gradio 6**: `theme=` is a `launch()` kwarg, **not** `gr.Blocks()`.
75
+ - **ruff isort gotcha**: not-yet-existing first-party modules classify as third-party (I001) in
76
+ pre-implementation test files — write imports in post-implementation order, ignore the early fail.
77
+ - **Contracts** (`docs/contracts/`, v1.1.0) are the source of truth: bump schema + golden samples +
78
+ all consumers in **one** PR (lockstep minor); label `contract-change` ⇒ orchestrator review.
79
+ - **No secrets, ever.** 1Password Connect (local dev) + HF Space secrets (deploy); gitleaks in CI.
80
+ Client-facing endpoints use **Tailnet MagicDNS HTTPS** (e.g. `https://mac-studio.tail48bab7.ts.net/…`),
81
+ never raw IPs.
82
+
83
+ ## Coordination (see KB `10-projects/small-cuts/coordination/`)
84
+ - **Roles:** orchestrator = **Opus 4.8** (assumed the lead vacated by **Fable 5**, pending Carlos's
85
+ confirmation) · implementer = **Codex (GPT-5.x)** · reviewer = **GLM / opencode** · eval judge =
86
+ **agy (Gemini)** · optional independent check = **GPT-5.x red-teams the plan**. Keep orchestrator ≠
87
+ implementer (different model families).
88
+ - **Peers:** codex, agy, agent, opencode, + Carlos.
89
+ - **Surfaces:** GitHub Project board **#8**; epics **#36** team-space / **#37** team-mobile /
90
+ **#38** team-inference; labels `team-*` + `contract-change`.
91
+
92
+ ## Pointers
93
+ - **KB tree** (via `knowledge-base` MCP, under `10-projects/small-cuts/`): `00-overview`,
94
+ `architecture/` (canonical + interactive `index.html`), `space/`, `mobile/`, `inference/`,
95
+ `contracts/`, `product/`, `coordination/`. Predecessor: `10-projects/directors-cut/`.
96
+ - **Live device test:** `ios/SmallCuts/RUNBOOK.md`. **Hackathon rules:** `docs/hackathon-rules.md`.
97
+ - **Live Space:** `build-small-hackathon/small-cuts`. **Model:** `Qwen/Qwen3-VL-8B-Instruct` (M1 pick).
README.md CHANGED
@@ -1,13 +1,139 @@
1
  ---
2
- title: Small Cuts Live
3
- emoji: 🏢
4
- colorFrom: red
5
- colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.18.0
8
- python_version: '3.13'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Small Cuts
3
+ emoji: 🎬
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.18.0
 
8
  app_file: app.py
9
+ pinned: true
10
+ license: mit
11
+ short_description: A deadpan narrator for your life, from small open models.
12
+ tags:
13
+ - track:wood
14
+ - achievement:offgrid
15
+ - achievement:offbrand
16
+ - achievement:llama
17
+ - achievement:fieldnotes
18
  ---
19
 
20
+ # Small Cuts 🎬
21
+
22
+ > *"And that was the moment Carlos realized the coffee had been decaf all along."*
23
+
24
+ **Small Cuts** turns first-person moments into grounded, cinematic, **spoken** narration —
25
+ an omniscient, slightly-too-honest narrator in the spirit of *The Invention of Lying* —
26
+ using only **small (≤32B) open models**. No script, no cloud LLM: a small vision-language
27
+ model watches your moment and a small TTS speaks the line, the way a film narrator would
28
+ if your life were the film.
29
+
30
+ There is exactly **one narrator** — a single deadpan, unnamed voice. No menus, no
31
+ director to pick. You point at what's happening; it tells you what it means.
32
+
33
+ This is the **challenger submission** for the
34
+ [Build Small Hackathon](https://huggingface.co/build-small-hackathon)
35
+ ("Small Models, Big Adventures" — Gradio × Hugging Face, submissions close
36
+ **June 15, 2026, 23:59 UTC**), the strategic successor to the original
37
+ *Director's Cut* project.
38
+
39
+ ## The soul of it — the live loop
40
+
41
+ Small Cuts was born wearing glasses. The intended experience is a **live loop**:
42
+
43
+ ```
44
+ Ray-Ban Meta glasses ──image frames──▶ home engine (small VLM + TTS) ──▶ narration in your ear
45
+
46
+ └──── finished cuts ────▶ the Space (watch · library)
47
+ ```
48
+
49
+ You walk through a moment; the narrator watches a selected first-person frame and speaks
50
+ one grounded, deadpan line back in your ear while the moment is still *recent past*. The
51
+ finished cut then lands in the Space as a short POV clip with synced captions, title, voice,
52
+ and library thumbnail.
53
+
54
+ **One pipeline, two surfaces:** the narration returns *in your ear* during capture, and lands
55
+ in the **Space** below as a finished cut you can re-watch — with film-style subtitles that
56
+ crawl in sync with the voice — and publish to a library.
57
+
58
+ ## What's in this Space
59
+
60
+ The Space is the **view platform + library** half of the loop — a small streaming-channel UI:
61
+
62
+ - **A live stage** with the current moment and **movie-style subtitles** (short
63
+ phrase-sized lines over a constant dark bar, advancing with the voice-over).
64
+ - **Voice-over replay**, with a compact custom player whose video, sound, captions, and progress
65
+ share the same audio clock.
66
+ - **A public library** of real Ray-Ban Meta glasses moments, generated through the same local
67
+ engine path so the channel is never empty. The source clips and mark points are curated; the
68
+ visible titles, narration, voice, thumbnails, and clips are produced by Small Cuts.
69
+ - **"Try it"** — a tucked-away sandbox (open only on request) to narrate your *own* short video.
70
+
71
+ ## How it was built
72
+
73
+ | Piece | Choice | Why |
74
+ |---|---|---|
75
+ | Narrator (VLM) | `Qwen/Qwen3-VL-8B-Instruct` | Strong grounded captioning at 8B — well under 32B |
76
+ | Voice (TTS) | **Kokoro** (24 kHz) | Tiny, expressive, open; one signature deadpan delivery |
77
+ | Space runtime | Gradio 6 on CPU, viewer-only for live demo | The judged canvas: public theater + library |
78
+ | Local live engine | FastAPI WS home node, **llama.cpp** | The in-ear loop + demo video; no cloud LLM/TTS API |
79
+ | Capture | iOS app for Ray-Ban Meta glasses (`ios/SmallCuts/`) | First-person moments, the way it's meant to be lived |
80
+
81
+ Built by **Carlos Crespo Macaya** as architect and lead. Development was accelerated with an
82
+ AI toolchain: Claude (Opus) for design critique, Codex (GPT-5.x) for paired implementation,
83
+ GLM for review, and Gemini for eval, all directed by Carlos.
84
+
85
+ ## Hackathon compliance
86
+
87
+ | Rule | How Small Cuts complies |
88
+ |---|---|
89
+ | Gradio app hosted as a Space under the org | The app **is** the product — this Space |
90
+ | Every model < 32B | 8B VLM narrator + small Kokoro TTS, all open weights |
91
+ | Demo video | Filmed POV with Ray-Ban Meta glasses → narrated by the app *(pending final link below)* |
92
+ | Social post | Linked from this README *(pending final link below)* |
93
+ | Track 2 — **Thousand Token Wood** (`track:wood`) | Whimsical, delightful, AI-load-bearing, original |
94
+ | Off the Grid (`achievement:offgrid`) | Live inference/TTS runs on local hardware; public Space reads finished cuts only |
95
+ | Llama (`achievement:llama`) | The live engine runs through `llama.cpp` |
96
+
97
+ - 📹 **Demo video:** _TODO — add public link before submission_
98
+ - 📣 **Social post:** _TODO — add link before submission_
99
+ - 📝 **Field notes:** [hf.co/blog/macayaven/small-cuts-field-notes](https://huggingface.co/blog/macayaven/small-cuts-field-notes)
100
+
101
+ **Bonus quests claimed:** Off-Brand (`offbrand`, custom cinematic frontend) · Off the Grid
102
+ (`offgrid`, local small-model engine for the live loop) · Llama (`llama`, llama.cpp) · Field
103
+ Notes (`fieldnotes`, the write-up above).
104
+
105
+ ## Quick start
106
+
107
+ ```bash
108
+ # install (CI-equivalent minimal)
109
+ uv sync --extra dev
110
+
111
+ # run the Space/viewer locally with the deterministic mock backend (no model download)
112
+ SMALL_CUTS_BACKEND=mock uv run --no-sync python app.py
113
+
114
+ # run with the real local VLM (downloads weights)
115
+ SMALL_CUTS_BACKEND=transformers uv run --no-sync python app.py
116
+
117
+ # run the real-time engine (needs `brew install llama.cpp`)
118
+ SMALL_CUTS_BACKEND=llama_cpp SMALL_CUTS_TTS_BACKEND=kokoro uv run python -m small_cuts.engine
119
+
120
+ # the gate (mirrors CI exactly)
121
+ uv run ruff check . && uv run ruff format --check . && uv run pytest
122
+ ```
123
+
124
+ ## Repository map
125
+
126
+ - `app.py` — Hugging Face Space entrypoint (Gradio CPU viewer/library)
127
+ - `src/small_cuts/` — the product: `viewer.py` (streaming viewer), `narrator.py` (VLM backends),
128
+ `tts.py` (Kokoro), `styles.py` (grounded prompt), `engine/` (real-time home node), `seed_media/`
129
+ - `ios/SmallCuts/` — the Ray-Ban Meta glasses capture app
130
+ - `docs/` — [hackathon rules](docs/hackathon-rules.md) · [architecture](docs/product/architecture.md) ·
131
+ [contracts](docs/contracts/) · [progress](docs/progress.md)
132
+ - `CLAUDE.md` — operational conventions (the canonical command list lives here)
133
+
134
+ ## Engineering discipline
135
+
136
+ - `main` is protected (PR-based workflow); CI runs ruff lint + format check, pytest, and a
137
+ gitleaks secret scan on every push/PR.
138
+ - **No secrets in the repo, ever.** Secrets live in 1Password Connect (local dev) and HF Space
139
+ secrets (deployment). Client-facing endpoints use Tailnet MagicDNS HTTPS, never raw IPs.
app.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face Space entrypoint for Small Cuts.
2
+
3
+ Local dev keeps the lazy/mock defaults; on a Space this module wires the real
4
+ backends eagerly so visitors never pay the model-load cost per click:
5
+
6
+ - ``import spaces`` happens before anything touches torch (ZeroGPU hijack).
7
+ - The narrator loads at module scope (ZeroGPU packs the weights at startup).
8
+ - The narration hot path runs under ``@spaces.GPU``.
9
+ - TTS runs inside @spaces.GPU workers too (kokoro's torch use poisons
10
+ worker forks if it ever runs in the main process).
11
+ """
12
+
13
+ import os
14
+ import sys
15
+ import warnings
16
+ from pathlib import Path
17
+
18
+ from starlette.exceptions import StarletteDeprecationWarning
19
+
20
+ ROOT = Path(__file__).resolve().parent
21
+ SRC = ROOT / "src"
22
+ if str(SRC) not in sys.path:
23
+ sys.path.insert(0, str(SRC))
24
+
25
+ warnings.filterwarnings(
26
+ "ignore",
27
+ message=r".*HTTP_422_UNPROCESSABLE_ENTITY.*HTTP_422_UNPROCESSABLE_CONTENT.*",
28
+ category=StarletteDeprecationWarning,
29
+ )
30
+
31
+ ON_SPACE = bool(os.environ.get("SPACE_ID"))
32
+ ENGINE_MODE = bool(os.environ.get("SMALL_CUTS_ENGINE_URL", "").strip())
33
+
34
+ try:
35
+ import spaces # noqa: F401 (must precede torch imports for ZeroGPU)
36
+ except ImportError: # local dev / CI: no ZeroGPU
37
+ spaces = None
38
+
39
+ if ON_SPACE and not ENGINE_MODE:
40
+ os.environ.setdefault("SMALL_CUTS_BACKEND", "transformers")
41
+ os.environ.setdefault("SMALL_CUTS_TTS_BACKEND", "kokoro")
42
+
43
+ from small_cuts import narrator # noqa: E402
44
+ from small_cuts.viewer import THEME, build_viewer_app # noqa: E402
45
+
46
+ # Eager load: download + pack weights at startup, not on the first click.
47
+ # The @spaces.GPU mark lives on the viewer's go-live handler (via ui._gpu;
48
+ # ZeroGPU's startup scan only finds GPU functions on what Gradio binds).
49
+ if not ENGINE_MODE:
50
+ _backend = narrator.get_backend()
51
+ if spaces is not None and _backend.name == "transformers":
52
+ _backend._load()
53
+
54
+ # In engine mode the Space is only a public reader for a private home-node engine, so it must not
55
+ # warm local model weights or expose upload narration controls. No main-process TTS pre-warm:
56
+ # kokoro's torch use must stay inside @spaces.GPU workers.
57
+ demo = build_viewer_app()
58
+
59
+ if __name__ == "__main__":
60
+ demo.launch(theme=THEME)
docs/architecture.md ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Small Cuts — Architecture
2
+
3
+ ## Principle
4
+
5
+ **The Space is the product.** Everything a judge touches runs inside the
6
+ Hugging Face Space. All other machines (Mac Studio, DGX Spark, phones, glasses)
7
+ are development, evaluation, and content-capture tools.
8
+
9
+ ```
10
+ ┌─────────────────────────── Hugging Face Space (judged artifact) ──────────────────────────┐
11
+ │ │
12
+ │ Gradio UI (custom cinematic theme — "Off-Brand") │
13
+ │ │ image / clip frame / webcam ┌──────────────────────────────┐ │
14
+ │ ▼ │ styles.py │ │
15
+ │ ui.py ──────► narrator.py ◄────────────────│ director presets + │ │
16
+ │ │ │ prompt builder │ │
17
+ │ ▼ └──────────────────────────────┘ │
18
+ │ Backend (pluggable, env SMALL_CUTS_BACKEND) │
19
+ │ ├─ mock deterministic, CI/tests, no weights │
20
+ │ ├─ transformers small VLM on ZeroGPU (e.g. SmolVLM2 / Qwen-VL ≤8B) │
21
+ │ └─ llama_cpp GGUF via llama.cpp (CPU fallback + "Llama Champion") │
22
+ │ │ │
23
+ │ ▼ │
24
+ │ TTS (small, local — M2) ──► audio + title card ──► shareable output │
25
+ └───────────────────────────────────────────────────────────────────────────────────────────┘
26
+
27
+ Capture sources (feed the demo & the UI, never load-bearing):
28
+ • Ray-Ban Meta glasses → footage exported via phone → demo video + sample inputs
29
+ • iPhone 14 Pro / Redmi Note 14 Pro+ → mobile browser upload (Gradio is responsive)
30
+ • Webcam tab in the UI
31
+
32
+ Live Mode (confirmed direction; continuous mode is stretch M3.5):
33
+ • Audio out: phone browser plays the Space's TTS → Bluetooth → glasses speakers (free)
34
+ • Continuous: phone camera → Gradio streaming → scene-change gate → narrate → TTS stream
35
+ • Glasses-camera capture via Meta Wearables DAT: post-hackathon, unverified
36
+
37
+ Dev/eval machinery (not judged):
38
+ • Mac Studio M4 Max → orchestration, dev, video editing
39
+ • DGX Spark 128GB → candidate-model evaluation harness, optional LoRA fine-tune
40
+ ("Well-Tuned" quest), batch frame preprocessing experiments
41
+ • 1Password Connect → local dev secrets; HF Space secrets for deployment
42
+ • Tailnet → access between Carlos's machines only
43
+ ```
44
+
45
+ ## Key decisions (and why)
46
+
47
+ | Decision | Choice | Rationale |
48
+ |---|---|---|
49
+ | Demo surface | Gradio Space (only) | Hackathon hard rule; judges must run it |
50
+ | Glasses role | Capture for demo video + sample content | Removes pairing fragility; preserves wow |
51
+ | Most reliable capture path | Any phone browser → upload | No native app needed in 4 days |
52
+ | Inference location | Inside the Space (ZeroGPU + CPU fallback) | "Off the Grid" quest; no cloud APIs |
53
+ | DGX Spark role | Model eval + optional fine-tune | Can't serve judges; perfect for picking the best ≤8B narrator and for "Well-Tuned" |
54
+ | Mac Studio role | Dev + demo video production | — |
55
+ | Backend abstraction | `SMALL_CUTS_BACKEND` env, 3 implementations | CI runs without GPU; llama.cpp path is a quest multiplier; model swap is one env var |
56
+ | Model candidates (M1 eval) | SmolVLM2-2.2B-Instruct · Qwen2.5-VL-3B/7B-Instruct · gemma-3-4b-it | All ≤8B, vision-capable, open weights; final pick by narration quality on a fixed eval set (unverified until run) |
57
+ | TTS candidates (M2) | Kokoro-82M · other ≤1B open TTS | Tiny, fast, runs CPU-side |
58
+
59
+ ## Secrets
60
+
61
+ No runtime secrets are required for the core app (local models only). HF token
62
+ for deployment lives in 1Password Connect locally and in GitHub Actions / Space
63
+ secrets remotely. `gitleaks` runs in CI on every push.
64
+
65
+ ## Verification commands
66
+
67
+ ```bash
68
+ uv run pytest # unit tests (mock backend)
69
+ uv run ruff check . && uv run ruff format --check .
70
+ SMALL_CUTS_BACKEND=mock uv run python app.py # UI smoke test
71
+ SMALL_CUTS_BACKEND=transformers uv run python app.py # real model (M1+)
72
+ ```
docs/contracts/CLAUDE.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md — `docs/contracts/` (inter-team contracts, v1.1.0)
2
+
3
+ Module-local notes for the **contract schemas** — the *only* coupling between the space, mobile, and
4
+ inference teams, and the **source of truth** for message shapes. Rationale + history are in the **KB**
5
+ (`10-projects/small-cuts/contracts/` and `…/architecture/`).
6
+
7
+ ## The four message types
8
+ - **MomentEnvelope** (mobile → engine) — `moment.schema.json`: frames (≤1024px) + gate + context.
9
+ - **ControlFrame** (engine → mobile) — `control.schema.json`: `ack`
10
+ (`accepted|duplicate|rejected|dropped_coalesced`), `error` (stage/code/retryable), `status`
11
+ (busy/queue_depth).
12
+ - **SceneAudio** (engine → mobile) — `scene-audio.schema.json`: audio + `play_by` freshness deadline.
13
+ - **NarratedScene** (engine → viewer & library, SSE) — `narrated-scene.schema.json`: `seq` (resume),
14
+ `captured_at`, narration, media URLs, `visibility`.
15
+
16
+ ## Enforcement
17
+ - Strict JSON Schema (`additionalProperties: false`) + **golden samples** in `tests/test_contracts.py`,
18
+ validated on every push (CI). Run locally: `uv run pytest tests/test_contracts.py`.
19
+
20
+ ## Change process (the one rule that matters)
21
+ - Bump the schema `$id` (semver) **+** golden samples **+** ALL consumers (Python engine/viewer +
22
+ iOS `MomentBuilder`/`EngineSessionClient`) **in ONE PR**. Additive = **minor, lockstep**; breaking =
23
+ **major + migration note**.
24
+ - Label the PR **`contract-change`** ⇒ **orchestrator review required**.
25
+ - The ≤6 s streamed-audio stretch is a **future MAJOR** version (cross-team), not a v1 toggle.
docs/contracts/README.md ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Inter-team contracts
2
+
3
+ These schemas are the **only coupling** between the three teams
4
+ ([product architecture](../product/architecture.md)). Each team optimizes
5
+ freely inside its boundary; the contracts keep the system aligned.
6
+ Current contract set: **1.1.0** (post adversarial review — Codex + Gemini,
7
+ 2026-06-12).
8
+
9
+ | Contract | Producer → Consumer | Schema |
10
+ |---|---|---|
11
+ | MomentEnvelope | Mobile → Engine | [moment.schema.json](moment.schema.json) |
12
+ | ControlFrame (ack / error / status) | Engine → Mobile | [control.schema.json](control.schema.json) |
13
+ | SceneAudio | Engine → Mobile | [scene-audio.schema.json](scene-audio.schema.json) |
14
+ | NarratedScene | Engine → Viewer + Library | [narrated-scene.schema.json](narrated-scene.schema.json) |
15
+
16
+ ## Transport bindings
17
+
18
+ ### Mobile ↔ Engine — one WebSocket per wearing session (`ws://<engine>/v1/session`)
19
+
20
+ - Client sends `MomentEnvelope` text frames. Engine answers **every**
21
+ envelope with a `ControlFrame{kind=ack}` whose `result` means *admission*:
22
+ `accepted` (in the pipeline), `duplicate` (idempotent resend ignored),
23
+ `rejected` (validation failure — never retry unchanged),
24
+ `dropped_coalesced` (replaced by a newer moment, D8).
25
+ - Completion is signaled by `SceneAudio` (success) or
26
+ `ControlFrame{kind=error}` (stage, code, retryable) — the app is never
27
+ blind to downstream failures.
28
+ - **Backpressure (D8):** engine emits `ControlFrame{kind=status}` with
29
+ `busy`/`queue_depth`; apps suppress the gate while `busy`. Engine policy:
30
+ queue depth ≤ 1, **coalesce-to-newest** (a queued, un-started moment is
31
+ replaced by a newer arrival and acked `dropped_coalesced` — narrating
32
+ stale moments is worse than skipping them).
33
+ - **Reconnect & idempotency:** after reconnect the app may resend envelopes
34
+ that never received an ack; the engine dedupes on `moment_id`. Chronology
35
+ authority is `captured_at`, not arrival order; `prev_moment_id` and `seq`
36
+ are advisory.
37
+ - **Playback (D9):** the app never overlaps clips; a clip whose `play_by`
38
+ passed before playback starts is dropped (engine default: `created_at`
39
+ + 60 s).
40
+
41
+ ### Engine → Viewer — SSE (`GET /v1/scenes/stream`)
42
+
43
+ - Emits `NarratedScene` with the SSE `id:` field set to `seq`; clients
44
+ resume with `Last-Event-ID`. Pipeline failures appear as
45
+ `ControlFrame{kind=error}` events on the same stream so the timeline
46
+ stays honest.
47
+ - Library reads: `GET /v1/scenes?session=…&visibility=…` (same schema,
48
+ ordered by `captured_at`).
49
+ - **Visibility mutations** (the viewer's only write — metadata, never
50
+ media): `PATCH /v1/scenes/{scene_id}` body
51
+ `{"visibility": "private"|"shared"|"public"}`. Auth + `share_token`
52
+ enforcement specs land with the library milestone (#30); v1 engines are
53
+ single-user on the tailnet, and `owner`/`share_token` fields are already
54
+ reserved in the schema so sharing is additive.
55
+ - Media URLs inside `NarratedScene.media` resolve against the engine origin.
56
+
57
+ ## Session context (D5)
58
+
59
+ The engine owns session memory. Capture apps contribute only what is inside
60
+ `MomentEnvelope.context` + the chronology chain (`captured_at`,
61
+ `prev_moment_id`). The prompt builder may use: recent narrations for the
62
+ session, time-of-day (via `tz_offset_min`), `location_label`, `user_hint`,
63
+ gate trigger. Token budget for injected context: ≤ 400 tokens (inference
64
+ team owns the selection policy — #20).
65
+
66
+ ## Enforcement
67
+
68
+ 1. **Schemas are the source of truth.** Implementation types are generated
69
+ from or validated against them — never hand-drifted.
70
+ 2. **Golden samples in CI.** Each producer keeps at least one golden sample
71
+ per message type; `tests/test_contracts.py` validates all samples
72
+ against these schemas on every push (jsonschema).
73
+ 3. **Versioning — strict + lockstep.** Schema version lives in the `$id`
74
+ path (`…/contracts/<semver>/…`). Schemas are strict
75
+ (`additionalProperties: false`) and live in ONE repo, so any field
76
+ change — additive included — is a **lockstep minor bump**: schemas,
77
+ golden samples, and all consumers update in the same `contract-change`
78
+ PR. Tolerant-reader semantics only become relevant if schemas are ever
79
+ vendored into separate repos; until then, drift fails CI loudly.
80
+ Breaking shape changes (removals, type changes, transport semantics)
81
+ are a major bump + migration note.
82
+ 4. **Change process.** Contract changes are PRs labeled `contract-change`,
83
+ reviewed by the orchestrator, announced in the KB
84
+ (`10-projects/small-cuts/`) before merge.
docs/contracts/control.schema.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/macayaven/small-cuts/docs/contracts/1.1.0/control.schema.json",
4
+ "title": "ControlFrame",
5
+ "description": "Engine -> capture-app control messages on the session WebSocket: envelope acknowledgement, pipeline errors, and backpressure status (D8). Every MomentEnvelope receives exactly one ack; errors and status frames may arrive any time.",
6
+ "type": "object",
7
+ "required": ["contract_version", "kind"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "contract_version": { "type": "string", "pattern": "^1\\." },
11
+ "kind": { "enum": ["ack", "error", "status"] },
12
+ "moment_id": {
13
+ "type": ["string", "null"],
14
+ "description": "Set for ack and error; null for status frames."
15
+ },
16
+ "ack": {
17
+ "type": "object",
18
+ "description": "kind=ack: the envelope's fate at admission (NOT completion - completion is the SceneAudio/NarratedScene or an error frame).",
19
+ "additionalProperties": false,
20
+ "properties": {
21
+ "result": { "enum": ["accepted", "duplicate", "rejected", "dropped_coalesced"] },
22
+ "detail": { "type": "string", "maxLength": 200 }
23
+ }
24
+ },
25
+ "error": {
26
+ "type": "object",
27
+ "description": "kind=error: a pipeline failure for an accepted moment, so the app is never blind to downstream failures.",
28
+ "additionalProperties": false,
29
+ "properties": {
30
+ "stage": { "enum": ["validation", "narration", "tts", "storage"] },
31
+ "code": { "type": "string", "maxLength": 60 },
32
+ "message": { "type": "string", "maxLength": 300 },
33
+ "retryable": { "type": "boolean" }
34
+ }
35
+ },
36
+ "status": {
37
+ "type": "object",
38
+ "description": "kind=status: backpressure signal. Apps suppress the gate while busy=true (D8).",
39
+ "additionalProperties": false,
40
+ "properties": {
41
+ "busy": { "type": "boolean" },
42
+ "queue_depth": { "type": "integer", "minimum": 0 }
43
+ }
44
+ }
45
+ }
46
+ }
docs/contracts/moment.schema.json ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/macayaven/small-cuts/docs/contracts/1.1.0/moment.schema.json",
4
+ "title": "MomentEnvelope",
5
+ "description": "One gated moment from a capture app to the narration engine (D1/D2). Sent as a WebSocket text frame (JSON); frame payloads are base64 JPEG.",
6
+ "type": "object",
7
+ "required": [
8
+ "contract_version",
9
+ "moment_id",
10
+ "session_id",
11
+ "captured_at",
12
+ "frames"
13
+ ],
14
+ "additionalProperties": false,
15
+ "properties": {
16
+ "contract_version": {
17
+ "type": "string",
18
+ "pattern": "^1\\."
19
+ },
20
+ "moment_id": {
21
+ "type": "string",
22
+ "format": "uuid"
23
+ },
24
+ "session_id": {
25
+ "type": "string",
26
+ "description": "One continuous wearing session (glasses on -> off)."
27
+ },
28
+ "captured_at": {
29
+ "type": "string",
30
+ "format": "date-time"
31
+ },
32
+ "frames": {
33
+ "type": "array",
34
+ "minItems": 1,
35
+ "maxItems": 24,
36
+ "description": "Selected narration frame first, followed by optional timestamped supplemental JPEG frames for a short just-happened POV clip. Longest decoded side must be <= 1024 px.",
37
+ "items": {
38
+ "type": "object",
39
+ "required": [
40
+ "jpeg_b64",
41
+ "width",
42
+ "height"
43
+ ],
44
+ "additionalProperties": false,
45
+ "properties": {
46
+ "jpeg_b64": {
47
+ "type": "string",
48
+ "contentEncoding": "base64"
49
+ },
50
+ "width": {
51
+ "type": "integer",
52
+ "maximum": 1024
53
+ },
54
+ "height": {
55
+ "type": "integer",
56
+ "maximum": 1024
57
+ },
58
+ "ts_offset_ms": {
59
+ "type": "integer",
60
+ "description": "Offset from captured_at for multi-frame moments."
61
+ }
62
+ }
63
+ }
64
+ },
65
+ "gate": {
66
+ "type": "object",
67
+ "description": "Why the gate fired - engine may use it for prompt salience; never required.",
68
+ "additionalProperties": false,
69
+ "properties": {
70
+ "scene_change_score": {
71
+ "type": "number",
72
+ "minimum": 0,
73
+ "maximum": 1
74
+ },
75
+ "motion_score": {
76
+ "type": "number",
77
+ "minimum": 0,
78
+ "maximum": 1
79
+ },
80
+ "trigger": {
81
+ "enum": [
82
+ "scene_change",
83
+ "interval",
84
+ "user",
85
+ "session_start"
86
+ ]
87
+ }
88
+ }
89
+ },
90
+ "context": {
91
+ "type": "object",
92
+ "description": "Capture-side context for the engine's session memory (D5).",
93
+ "additionalProperties": false,
94
+ "properties": {
95
+ "location_label": {
96
+ "type": "string",
97
+ "maxLength": 120
98
+ },
99
+ "user_hint": {
100
+ "type": "string",
101
+ "maxLength": 280
102
+ },
103
+ "style_key": {
104
+ "type": "string",
105
+ "description": "Director's cut requested for this session; engine default if absent."
106
+ },
107
+ "battery_pct": {
108
+ "type": "integer",
109
+ "minimum": 0,
110
+ "maximum": 100
111
+ },
112
+ "network": {
113
+ "enum": [
114
+ "wifi",
115
+ "cellular",
116
+ "tailnet"
117
+ ]
118
+ },
119
+ "tz_offset_min": {
120
+ "type": "integer",
121
+ "minimum": -840,
122
+ "maximum": 840
123
+ },
124
+ "orientation": {
125
+ "enum": [
126
+ "portrait",
127
+ "landscape",
128
+ "portrait_upside_down",
129
+ "landscape_flipped"
130
+ ]
131
+ }
132
+ }
133
+ },
134
+ "prev_moment_id": {
135
+ "type": [
136
+ "string",
137
+ "null"
138
+ ],
139
+ "description": "Chain for the engine's chronology; null at session start."
140
+ },
141
+ "seq": {
142
+ "type": "integer",
143
+ "minimum": 0,
144
+ "description": "Monotonic per-session counter for diagnosis; ordering authority is captured_at (engine dedupes on moment_id - idempotent resends after reconnect are safe)."
145
+ },
146
+ "sent_at": {
147
+ "type": "string",
148
+ "format": "date-time",
149
+ "description": "Upload instant, for capture->engine latency attribution."
150
+ }
151
+ }
152
+ }
docs/contracts/narrated-scene.schema.json ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/macayaven/small-cuts/docs/contracts/1.1.0/narrated-scene.schema.json",
4
+ "title": "NarratedScene",
5
+ "description": "One narrated scene, produced by the engine. The SAME object is the viewer's live SSE event (D7) and the library's stored entry (D6); media fields are engine-served URLs.",
6
+ "type": "object",
7
+ "required": [
8
+ "captured_at",
9
+ "contract_version",
10
+ "created_at",
11
+ "media",
12
+ "moment_id",
13
+ "narration",
14
+ "scene_id",
15
+ "seq",
16
+ "session_id",
17
+ "style_key",
18
+ "title",
19
+ "visibility"
20
+ ],
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "contract_version": {
24
+ "type": "string",
25
+ "pattern": "^1\\."
26
+ },
27
+ "scene_id": {
28
+ "type": "string",
29
+ "format": "uuid"
30
+ },
31
+ "moment_id": {
32
+ "type": "string",
33
+ "format": "uuid"
34
+ },
35
+ "session_id": {
36
+ "type": "string"
37
+ },
38
+ "created_at": {
39
+ "type": "string",
40
+ "format": "date-time"
41
+ },
42
+ "style_key": {
43
+ "type": "string"
44
+ },
45
+ "title": {
46
+ "type": "string",
47
+ "maxLength": 80,
48
+ "description": "Model-generated short title for the slate/thumbnail text; engines may fall back to derive_title for legacy or malformed model output."
49
+ },
50
+ "narration": {
51
+ "type": "string",
52
+ "maxLength": 2000
53
+ },
54
+ "visibility": {
55
+ "enum": [
56
+ "private",
57
+ "shared",
58
+ "public"
59
+ ],
60
+ "default": "private"
61
+ },
62
+ "media": {
63
+ "type": "object",
64
+ "required": [
65
+ "frame_url"
66
+ ],
67
+ "additionalProperties": false,
68
+ "properties": {
69
+ "frame_url": {
70
+ "type": "string",
71
+ "format": "uri-reference"
72
+ },
73
+ "card_url": {
74
+ "type": "string",
75
+ "format": "uri-reference"
76
+ },
77
+ "audio_url": {
78
+ "type": "string",
79
+ "format": "uri-reference"
80
+ },
81
+ "clip_url": {
82
+ "type": "string",
83
+ "format": "uri-reference",
84
+ "description": "Short viewer clip for library revisit/share, either uploaded or assembled by the engine from timestamped capture frames."
85
+ }
86
+ }
87
+ },
88
+ "engine": {
89
+ "type": "object",
90
+ "description": "Provenance + latency breakdown (the inference team's scoreboard).",
91
+ "additionalProperties": false,
92
+ "properties": {
93
+ "narrator_model": {
94
+ "type": "string"
95
+ },
96
+ "narrator_backend": {
97
+ "enum": [
98
+ "llama_cpp",
99
+ "transformers",
100
+ "mock"
101
+ ]
102
+ },
103
+ "tts_model": {
104
+ "type": "string"
105
+ },
106
+ "latency_ms": {
107
+ "type": "object",
108
+ "additionalProperties": false,
109
+ "properties": {
110
+ "queue": {
111
+ "type": "integer"
112
+ },
113
+ "narration": {
114
+ "type": "integer"
115
+ },
116
+ "tts": {
117
+ "type": "integer"
118
+ },
119
+ "total": {
120
+ "type": "integer"
121
+ }
122
+ }
123
+ }
124
+ }
125
+ },
126
+ "seq": {
127
+ "type": "integer",
128
+ "minimum": 0,
129
+ "description": "Monotonic engine event id - SSE id: field; clients resume with Last-Event-ID."
130
+ },
131
+ "captured_at": {
132
+ "type": "string",
133
+ "format": "date-time",
134
+ "description": "From the source MomentEnvelope - the library's chronology key (created_at is engine time)."
135
+ },
136
+ "owner": {
137
+ "type": "string",
138
+ "description": "Reserved for multi-user: identity that owns the scene. v1 single-user engines emit a constant."
139
+ },
140
+ "share_token": {
141
+ "type": [
142
+ "string",
143
+ "null"
144
+ ],
145
+ "description": "Reserved: capability token for visibility=shared links. Enforcement spec lands with the library milestone."
146
+ }
147
+ }
148
+ }
docs/contracts/scene-audio.schema.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/macayaven/small-cuts/docs/contracts/1.1.0/scene-audio.schema.json",
4
+ "title": "SceneAudio",
5
+ "description": "Voice-over returned from the engine to the capture app for in-ear playback (D4). One complete clip per scene in 1.x; streamed delivery is a future MAJOR version with its own chunk semantics. Playback policy is D9: never overlap; drop if past play_by.",
6
+ "type": "object",
7
+ "required": [
8
+ "contract_version",
9
+ "scene_id",
10
+ "moment_id",
11
+ "created_at",
12
+ "play_by",
13
+ "format",
14
+ "audio_b64"
15
+ ],
16
+ "additionalProperties": false,
17
+ "properties": {
18
+ "contract_version": { "type": "string", "pattern": "^1\\." },
19
+ "scene_id": { "type": "string", "format": "uuid" },
20
+ "moment_id": { "type": "string", "format": "uuid" },
21
+ "created_at": { "type": "string", "format": "date-time" },
22
+ "play_by": {
23
+ "type": "string",
24
+ "format": "date-time",
25
+ "description": "Freshness deadline (D9): if playback has not started by this instant, the app drops the clip (engine default: created_at + 60s)."
26
+ },
27
+ "format": { "enum": ["wav_complete"] },
28
+ "audio_b64": { "type": "string", "contentEncoding": "base64" },
29
+ "sample_rate": { "type": "integer", "default": 24000 },
30
+ "narration": {
31
+ "type": "string",
32
+ "description": "The text being spoken - lets the app show captions without waiting for the viewer."
33
+ }
34
+ }
35
+ }
docs/eval/prompt-ab-comparison.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Prompt A/B: judged comparison of v1 (run-002) vs v2 (run-004)
2
+
3
+ Judge: Codex (GPT-5 vision), scoring every narration against the actual photo,
4
+ rubric S/G/V 1-5, pick rule S>=4 and G>=4. Same 10 photos, same judge, same
5
+ model (gemma-3-4b-it) — only the system prompt differs between the two runs.
6
+
7
+ ## Result
8
+
9
+ | Variant | n | mean S | mean G | mean V | S>=4 & G>=4 |
10
+ |---|---|---|---|---|---|
11
+ | gemma-3-4b-it @ v1 prompt (run-002, strict) | 30 | 3.37 | 2.57 | 2.53 | 2/30 |
12
+ | gemma-3-4b-it @ v2 prompt (run-004, "find the story") | 30 | 2.60 | 1.90 | 2.53 | 0/30 |
13
+ | Qwen2.5-VL-3B @ v2 prompt (run-004) | 30 | 2.73 | 2.70 | 2.50 | 4/30 |
14
+
15
+ ## Conclusions
16
+
17
+ 1. **v2 was a pure loss**: −0.77 S, −0.67 G, ±0.00 V for gemma. The style
18
+ examples already carry the voice; the extra "find the story / lead with the
19
+ human moment" license only licensed confabulation (invented rain, people,
20
+ actions). Reverted and hardened as v3 in `styles.py`.
21
+ 2. **Model size is the real ceiling**: even strict v1 passes only 2/30 cells.
22
+ Qwen2.5-VL-3B is the best so far (G 2.70). Qwen2.5-VL-7B is the most
23
+ important untested candidate (was blocked on torchvision, fixed in run-003).
24
+ 3. **Generation temperature was 0.7 with sampling in both runs** — a second
25
+ G-suppressor, lowered to 0.3 default (SMALL_CUTS_TEMPERATURE) for run-005.
26
+ 4. run-005 bundles prompt v3 + temp 0.3 + Qwen-7B. The v3-vs-v1 deltas are
27
+ confounded with the temperature change — accepted; both push G the same
28
+ way and the deadline is June 15.
29
+
30
+ ## Method note
31
+
32
+ Scoring loop is fully automated: `fetch-eval-photos` workflow relays photos
33
+ off the Spark → HEIC→JPEG → one `codex exec -i <photo>` call per image with
34
+ the report's narrations → strict-JSON S/G/V → aggregate. Re-running a scoring
35
+ pass on a new eval report costs ~7 minutes and zero manual effort.
docs/eval/run-002-gemma-baseline.md ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Small Cuts — M1 Narrator Model Eval
2
+
3
+ Generated 2026-06-11 21:13:27.
4
+
5
+ Score each cell 1-5 on: **S**pecificity (names real visible things), **G**roundedness (no invented objects/people), **V**oice (style lands). A model needs S>=4 and G>=4 on most images to be the pick.
6
+
7
+ ## HEIF Image-46AC-B1CB-06-0.heic
8
+
9
+ | Model | Style | Narration | Latency | S | G | V |
10
+ |---|---|---|---|---|---|---|
11
+ | google/gemma-3-4b-it | deadpan | A white wall is painted a dark gray. A security camera is mounted centrally on the wall. A blue plaque displays the rating “8.6” and the text “Mummy by Pillow.” Below the plaque, a rectangular white card sits on a small, terracotta pot containing dried lavender. A map of the area is pinned to the wall beside a collection of photographs depicting buildings and streets. | 155.7s | | | |
12
+ | google/gemma-3-4b-it | noir | A white wall dominates the frame, painted a matte gray. A small, framed review card displays the number 8.6 and the words “Mum by Pillow.” Below it, a map of the city is pinned alongside a collection of black and white photographs. A promotional flyer for “Enjoy the Magic of Musical Theatre” is affixed to the right, and a small, terracotta pot holds a sprig of lavender. A security camera is mounted above the wall, its lens pointed downward. | 5.8s | | | |
13
+ | google/gemma-3-4b-it | nature_doc | A white wall dominates the frame, its surface reflecting the light from a recessed ceiling fixture. A small, rectangular plaque displays the rating “8.6” beneath the text “Mummer by Pillow.” Below that, a collection of photographs depicts a cityscape, and a map of the area is pinned to a corkboard. A potted plant with lavender flowers sits on a small, dark table to the left of the wall. | 5.0s | | | |
14
+
15
+ ## HEIF Image-46AC-B1CB-06-1.heic
16
+
17
+ | Model | Style | Narration | Latency | S | G | V |
18
+ |---|---|---|---|---|---|---|
19
+ | google/gemma-3-4b-it | deadpan | A woman stands before a brown wooden counter. She wears a black skirt and a black and white striped shirt, along with white sneakers. A printed sign on the counter reads “Reparación de PC y Tabletas” in Spanish. Behind her, shelves hold a collection of printed materials, including a laptop and a printer. | 3.9s | | | |
20
+ | google/gemma-3-4b-it | noir | A woman in a black skirt and a striped shirt stands before a wooden counter. She wears white sneakers and a denim jacket. A sign reads “REPARACION DE PC Y TELEFATAS” in Spanish. A laptop sits on the counter, and a small, framed photograph is visible on the wall behind her. | 3.8s | | | |
21
+ | google/gemma-3-4b-it | nature_doc | A woman stands before a wooden counter, the surface displaying a sign in Spanish. She wears a black skirt and a black and white striped shirt, and white sneakers. A laptop sits on the counter, facing her. To the left, a shelf holds stacks of printed documents, and a small, framed photograph is visible on the wall behind. | 4.1s | | | |
22
+
23
+ ## HEIF Image-46AC-B1CB-06-2.heic
24
+
25
+ | Model | Style | Narration | Latency | S | G | V |
26
+ |---|---|---|---|---|---|---|
27
+ | google/gemma-3-4b-it | deadpan | A white Ford Focus occupies the lower portion of the frame, its tires resting on a gray asphalt surface. A shadow stretches from the car’s underside, cast by the low angle of the afternoon sun. Brick buildings line the street, displaying darkened windows and a sign reading “Centro de Dia” above a doorway. A small, yellow parking sign with a red circle and diagonal slash is affixed to a white metal railing beside the road. | 5.2s | | | |
28
+ | google/gemma-3-4b-it | noir | A white Ford Focus occupies the right side of the frame, its tires resting on a grey asphalt surface. A shadow stretches from the vehicle’s underside, cast by the late afternoon sun. Across the street, a brick building rises, punctuated by rectangular windows and a wrought iron balcony. A small, yellow sign, partially obscured, reads “Plaza de Centro de Salud.” | 4.5s | | | |
29
+ | google/gemma-3-4b-it | nature_doc | A white Ford Focus occupies the right foreground, its chrome hubcaps reflecting the overcast sky. A shadow stretches from the vehicle’s lower portion across the grey pavement. Across the street, a brick building rises with a series of rectangular windows and a white wrought iron fence. A small, yellow sign is affixed to the building’s façade, displaying text in Spanish. | 4.4s | | | |
30
+
31
+ ## HEIF Image-46AC-B1CB-06-3.heic
32
+
33
+ | Model | Style | Narration | Latency | S | G | V |
34
+ |---|---|---|---|---|---|---|
35
+ | google/gemma-3-4b-it | deadpan | A dark laptop rests on a rumpled gray blanket. A white paper lantern hangs above, its shade casting a diffuse light across the ceiling. A denim jacket is draped over a wooden hanger beside the bed. | 2.6s | | | |
36
+ | google/gemma-3-4b-it | noir | A laptop rests open on a bed, the screen dark. A patterned sheet lies rumpled beneath it, and a denim jacket hangs on a wooden hanger. Above, a white paper lantern illuminates the ceiling, and a vent grille is visible in the plaster. | 3.1s | | | |
37
+ | google/gemma-3-4b-it | nature_doc | A dark-gray laptop rests on a bedspread of pale blue and white. A white ceiling hangs above, punctuated by a hanging light fixture with a white, paper-like shade. A denim jacket is draped over a wooden clothes rack to the right. | 3.2s | | | |
38
+
39
+ ## HEIF Image-46AC-B1CB-06-4.heic
40
+
41
+ | Model | Style | Narration | Latency | S | G | V |
42
+ |---|---|---|---|---|---|---|
43
+ | google/gemma-3-4b-it | deadpan | A man sits at a wooden table, wearing sunglasses and a black t-shirt. A small, dark pipe rests in his hand, and he holds a glass of amber liquid. Two bottles of Coca-Cola and a square, red “Gracias” box are positioned on the table surface. A glass of golden liquid sits to the right of the pipe and bottle. | 4.4s | | | |
44
+ | google/gemma-3-4b-it | noir | A man sits at a dark wooden table. He wears a black shirt and sunglasses, and a small, white pipe rests in his hand. Two bottles of Coca-Cola and a glass of Estrella beer sit on the table beside him, along with a small, red box that reads “Gracias.” Outside, a blue sky is visible through a leafy hedge, and a building with beige facades rises in the background. | 4.9s | | | |
45
+ | google/gemma-3-4b-it | nature_doc | A man sits at a gray outdoor table. He wears sunglasses and a black shirt with “Training” printed on the sleeve. A lit pipe rests in his mouth, and a dark-colored bottle rests on the table beside him. Two glasses of amber liquid sit on the table, one partially full, the other nearly empty. | 4.0s | | | |
46
+
47
+ ## HEIF Image-46AC-B1CB-06-5.heic
48
+
49
+ | Model | Style | Narration | Latency | S | G | V |
50
+ |---|---|---|---|---|---|---|
51
+ | google/gemma-3-4b-it | deadpan | A man in a dark gray t-shirt stands before a glass door. His hands are resting on the cool, gray concrete of a raised platform. A red neon sign reading “D&G” is visible through the glass, illuminating a dark interior. A printed sign on the door advertises “Dog Friendly” and lists daily hours. | 4.2s | | | |
52
+ | google/gemma-3-4b-it | noir | A man stands before a glass door, his hands resting on the cool metal of the threshold. He wears a dark t-shirt and his hair is graying at the temples. A red sign, partially obscured by the glass, reads “D&G Friendly.” The interior of a building is visible through the glass, showing a dimly lit space with tables and chairs. | 4.5s | | | |
53
+ | google/gemma-3-4b-it | nature_doc | A man stands before a glass door, his hand resting on the cool metal of the threshold. He wears a dark gray t-shirt and his hair is graying at the temples. The glass door reflects the interior of a brightly lit restaurant, showing a red awning and the words “D&G Friendly.” A small, hand-written sign above the door reads “De Dimous A Diurne.” | 4.9s | | | |
54
+
55
+ ## HEIF Image-46AC-B1CB-06-6.heic
56
+
57
+ | Model | Style | Narration | Latency | S | G | V |
58
+ |---|---|---|---|---|---|---|
59
+ | google/gemma-3-4b-it | deadpan | A light fixture with a circular diffuser emits a bright, white glow. A woven, spherical light hangs below it, casting a diffuse shadow on the wall. A gray, upholstered chair sits to the right, with a small, woven basket on its surface. A partial view of a dark wooden door is visible in the foreground. | 4.0s | | | |
60
+ | google/gemma-3-4b-it | noir | A circular ceiling light illuminates a section of the room, casting a stark white circle on the wall. A woven pendant light hangs below, its shade partially obscuring the bulb. A gray wall is visible to the right, with a recessed door frame. A portion of a patterned rug lies in the foreground, beneath a hand. | 3.9s | | | |
61
+ | google/gemma-3-4b-it | nature_doc | A pale circular light illuminates the ceiling. A woven light fixture hangs directly below, casting a patterned shadow on the wall. A grey sofa sits partially visible in the background, draped with a woven throw. The lower portion of a person’s hand rests on a patterned blanket. | 3.4s | | | |
62
+
63
+ ## HEIF Image-46AC-B1CB-06-7.heic
64
+
65
+ | Model | Style | Narration | Latency | S | G | V |
66
+ |---|---|---|---|---|---|---|
67
+ | google/gemma-3-4b-it | deadpan | A black backpack rests on the top of a white cabinet. The cabinet doors are closed, revealing a stainless steel oven and a blank, white shelf. A metal key hangs from a keyhole on the cabinet’s right side. Gray-toned wooden flooring extends across the lower portion of the frame. | 3.7s | | | |
68
+ | google/gemma-3-4b-it | noir | A black backpack rests on the surface of a white cabinet. The cabinet doors are closed, revealing the interior of the appliance and a blank, white space. A silver oven control panel is visible within the cabinet’s opening. A light gray floor runs beneath the cabinet, extending into the distance. | 3.6s | | | |
69
+ | google/gemma-3-4b-it | nature_doc | A black backpack rests on the top shelf of a white cabinet. The cabinet doors are closed, revealing a stainless steel oven with a mesh filter and a lower shelf. A light gray floor with a wood-grain pattern extends into the frame. A silver handle is visible on the cabinet door. | 3.6s | | | |
70
+
71
+ ## HEIF Image-46AC-B1CB-06-8.heic
72
+
73
+ | Model | Style | Narration | Latency | S | G | V |
74
+ |---|---|---|---|---|---|---|
75
+ | google/gemma-3-4b-it | deadpan | A white parasol shields a small table and four chairs from direct sunlight. A large, mature tree occupies the center of the frame, its branches extending over the seating area. A man sits at the table, facing away from the camera, wearing a dark blue shirt and white pants. Several bicycles are parked along the sidewalk in the background, and a green garbage bin sits to the left of the tree. | 4.9s | | | |
76
+ | google/gemma-3-4b-it | noir | A white parasol extends over a small table with four chairs. A man sits at the table, facing the street. A dark tree trunk dominates the left side of the frame, its branches covered in dense green leaves. Several bicycles are parked along the sidewalk, and a green waste bin sits beneath a building on the right. | 4.0s | | | |
77
+ | google/gemma-3-4b-it | nature_doc | A large white umbrella provides shade over a small table and four chairs. A man sits at the table, facing away from the camera. Green foliage overhangs the table and extends upward, partially obscuring a building on the left. A dark green trash receptacle stands to the left of the table. | 3.6s | | | |
78
+
79
+ ## HEIF Image-46AC-B1CB-06-9.heic
80
+
81
+ | Model | Style | Narration | Latency | S | G | V |
82
+ |---|---|---|---|---|---|---|
83
+ | google/gemma-3-4b-it | deadpan | A man sits at a wooden desk. He wears a gray, long-sleeved sweater and dark pants. A white plant sits on the desk to his left, and a folded document rests on the surface in front of him. A gray staircase curves upwards to the right, with a pale gray railing. | 3.7s | | | |
84
+ | google/gemma-3-4b-it | noir | A man sits at a wooden desk, his dark hair pulled back from his face. A beige document rests on the desk before him, partially obscured by a small, white branch. The steps leading upward are gray concrete, illuminated by a bright, rectangular window. A black elevator panel is mounted on the wall behind the man. | 4.0s | | | |
85
+ | google/gemma-3-4b-it | nature_doc | A dark-colored sweater covers the figure’s torso. The individual’s dark hair is pulled back from their face and rests against a wooden surface. A light-colored document is positioned on the surface in front of the individual. A white branch, covered in small green leaves, sits on a wooden box to the left. | 4.0s | | | |
86
+
87
+ ## Failed models
88
+
89
+ - HuggingFaceTB/SmolVLM2-2.2B-Instruct: ValueError: Unrecognized image processor in HuggingFaceTB/SmolVLM2-2.2B-Instruct. Should have a `image_processor_type` key in its preprocessor_config.json of config.json, or one of the following `model_type` keys in its config.json: aria, beit, bit, blip, bridgetower, chameleon, chinese_clip, chmv2, clip, cohere2_vision, conditional_detr, convnext, deepseek_ocr2, deepseek_vl, deepseek_vl_hybrid, deformable_detr, deit, depth_pro, detr, dinov3_vit, dpt, efficientloftr, efficientnet, eomt, ernie4_5_vl_moe, flava, fuyu, gemma3, gemma4, gemma4_unified, glm46v, glm4v, glm_image, glmga, glpn, got_ocr2, grounding-dino, idefics, idefics2, idefics3, imagegpt, janus, layoutlmv2, layoutlmv3, levit, lfm2_vl, lightglue, llama4, llava, llava_next, llava_onevision, mask2former, maskformer, minicpmv4_6, mllama, mobilenet_v1, mobilenet_v2, mobilevit, nougat, oneformer, ovis2, owlv2, owlvit, paddleocr_vl, perceiver, perception_lm, phi4_multimodal, pi0, pix2struct, pixtral, poolformer, pp_chart2table, pp_doclayout_v2, pp_doclayout_v3, pp_formulanet, pp_lcnet, pp_ocrv5_server_det, pp_ocrv5_server_rec, pp_ocrv6_small_rec, prompt_depth_anything, pvt, qwen2_vl, rf_detr, rt_detr, sam, sam2, sam3, sapiens2, segformer, seggpt, siglip, siglip2, slanext, smolvlm, superglue, superpoint, swin2sr, textnet, tvp, uvdoc, video_llama_3, videomae, vilt, vit, vitmatte, vitpose, yolos, zoedepth, aimv2, aimv2_vision_model, align, altclip, aya_vision, blip-2, clipseg, colpali, colqwen2, convnextv2, cosmos3_omni, cvt, data2vec-vision, deimv2, depth_anything, dinat, dinov2, donut-swin, edgetam, emu3, eomt_dinov3, exaone4_5, florence2, focalnet, gemma3n, git, granite4_vision, groupvit, hiera, ijepa, instructblip, internvl, kosmos-2, kosmos-2.5, layoutxlm, lighton_ocr, llava_next_video, lw_detr, metaclip_2, mgp-str, mistral3, mlcd, mm-grounding-dino, mobilevitv2, omdet-turbo, paligemma, pixio, pp_ocrv5_mobile_det, pp_ocrv5_mobile_rec, pp_ocrv6_medium_det, pp_ocrv6_small_det, pp_ocrv6_tiny_rec, pvt_v2, qianfan_ocr, qwen2_5_omni, qwen2_5_vl, qwen3_5, qwen3_5_moe, qwen3_omni_moe, qwen3_vl, regnet, resnet, sam2_video, sam3_lite_text, sam3_tracker, sam3_tracker_video, sam3_video, sam_hq, shieldgemma2, slanet, swiftformer, swin, swinv2, t5gemma2, t5gemma2_encoder, table-transformer, timesformer, timm_wrapper, trocr, udop, upernet, video_llava, vipllava, vit_mae, vit_msn, vivit, xclip
90
+ - Qwen/Qwen2.5-VL-3B-Instruct: ImportError:
91
+ Qwen2VLVideoProcessor requires the Torchvision library but it was not found in your environment. Check out the instructions on the
92
+ installation page: https://pytorch.org/get-started/locally/ and follow the ones that match your environment.
93
+ Please note that you may need to restart your runtime after installation.
94
+
95
+ - Qwen/Qwen2.5-VL-7B-Instruct: ImportError:
96
+ Qwen2VLVideoProcessor requires the Torchvision library but it was not found in your environment. Check out the instructions on the
97
+ installation page: https://pytorch.org/get-started/locally/ and follow the ones that match your environment.
98
+ Please note that you may need to restart your runtime after installation.
99
+
docs/eval/run-004-scored.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Small Cuts — M1 Eval, Scored (run-004)
2
+
3
+ Judge: Codex (GPT-5 vision) scoring each narration against the actual photo.
4
+ Rubric: S(pecificity), G(roundedness), V(oice), 1-5. Pick rule: S>=4 and G>=4 on most images.
5
+
6
+ ## Verdict
7
+
8
+ | Model | mean S | mean G | mean V | cells S>=4 & G>=4 | images passing (>=2/3 styles) |
9
+ |---|---|---|---|---|---|
10
+ | Qwen/Qwen2.5-VL-3B-Instruct | 2.73 | 2.70 | 2.50 | 4/30 | 1/10 |
11
+ | google/gemma-3-4b-it | 2.60 | 1.90 | 2.53 | 0/30 | 0/10 |
12
+
13
+ ## Per-cell scores
14
+
15
+ | Image | Model | Style | S | G | V | Judge note |
16
+ |---|---|---|---|---|---|---|
17
+ | img-46AC-B1CB-06-0 | google/gemma-3-4b-it | deadpan | 2 | 1 | 2 | Invents man, coffee, mug, robin; only rating and map align |
18
+ | img-46AC-B1CB-06-0 | google/gemma-3-4b-it | noir | 3 | 2 | 4 | Good noir mood, but invents a person and actions |
19
+ | img-46AC-B1CB-06-0 | google/gemma-3-4b-it | nature_doc | 2 | 1 | 2 | Invents hand, person, motion, dust, Tuscan photo |
20
+ | img-46AC-B1CB-06-0 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 5 | 4 | 4 | Mostly accurate specifics; “digital sign” and office framing are shaky |
21
+ | img-46AC-B1CB-06-0 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 2 | 3 | 4 | Noir lands, but stack of papers and desk are unsupported |
22
+ | img-46AC-B1CB-06-0 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 1 | 1 | 3 | Invents male, coffee machine, cups, and ritual |
23
+ | img-46AC-B1CB-06-1 | google/gemma-3-4b-it | deadpan | 2 | 2 | 3 | Brochures, travel guides, expression, and transaction are unsupported. |
24
+ | img-46AC-B1CB-06-1 | google/gemma-3-4b-it | noir | 2 | 1 | 4 | Invents stapler, rain, window, tracing fingers, and emotional claims. |
25
+ | img-46AC-B1CB-06-1 | google/gemma-3-4b-it | nature_doc | 3 | 2 | 1 | Sign is close, but paperback, tremor, and blue screen are invented. |
26
+ | img-46AC-B1CB-06-1 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 5 | 4 | 4 | Strong visible details; clothing and sign wording are slightly off. |
27
+ | img-46AC-B1CB-06-1 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 4 | 4 | 3 | Mostly grounded, but adds inner states and overstates noir mood. |
28
+ | img-46AC-B1CB-06-1 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 1 | 2 | 1 | Generic and invents walking, turning away, urgency, and sequence. |
29
+ | img-46AC-B1CB-06-2 | google/gemma-3-4b-it | deadpan | 3 | 2 | 2 | Some visible shadow/car details, but invents damp pavement, sign text, man watching. |
30
+ | img-46AC-B1CB-06-2 | google/gemma-3-4b-it | noir | 3 | 1 | 4 | Noir mood lands, but wet asphalt, overcast sky, cone, pothole, sign text invented. |
31
+ | img-46AC-B1CB-06-2 | google/gemma-3-4b-it | nature_doc | 2 | 2 | 1 | Mostly not nature-documentary; invents wet pavement, overcast sky, clinic door focus. |
32
+ | img-46AC-B1CB-06-2 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 2 | 4 | 1 | Mentions visible red car and shadows, but tone is ominous, not deadpan. |
33
+ | img-46AC-B1CB-06-2 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 2 | 2 | 3 | Some glare and shadow fit, but invents a man walking past. |
34
+ | img-46AC-B1CB-06-2 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 2 | 4 | 2 | Mostly grounded but generic; weak nature-documentary framing. |
35
+ | img-46AC-B1CB-06-3 | google/gemma-3-4b-it | deadpan | 4 | 3 | 3 | Good objects, but invents chair/person mood. |
36
+ | img-46AC-B1CB-06-3 | google/gemma-3-4b-it | noir | 3 | 4 | 4 | Mostly grounded, noir mood lands. |
37
+ | img-46AC-B1CB-06-3 | google/gemma-3-4b-it | nature_doc | 3 | 3 | 1 | Grounded objects, but not nature-documentary voice. |
38
+ | img-46AC-B1CB-06-3 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 3 | 2 | 2 | Invents desk, door, and air-conditioner sound. |
39
+ | img-46AC-B1CB-06-3 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 2 | 1 | 3 | Invents sleeping user and exaggerated lighting. |
40
+ | img-46AC-B1CB-06-3 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 3 | 3 | 1 | Some specifics, but invents figure and lacks documentary style. |
41
+ | img-46AC-B1CB-06-4 | google/gemma-3-4b-it | deadpan | 2 | 1 | 2 | Invents cigarette, smoke, Coke bottle, unopened beer, and shirt location. |
42
+ | img-46AC-B1CB-06-4 | google/gemma-3-4b-it | noir | 3 | 2 | 4 | Noir voice works, but cigarette packet and Coke bottle are invented. |
43
+ | img-46AC-B1CB-06-4 | google/gemma-3-4b-it | nature_doc | 3 | 2 | 1 | Several visible anchors, but cigarette, smoke, condensation, and sleeve text are wrong. |
44
+ | img-46AC-B1CB-06-4 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 5 | 3 | 3 | Specific, but mislabels tray and invents prior visitor and precarious cap. |
45
+ | img-46AC-B1CB-06-4 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 5 | 4 | 2 | Mostly grounded specifics; beverage color is wrong and noir voice is weak. |
46
+ | img-46AC-B1CB-06-4 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 4 | 3 | 2 | Uses real objects, but invents untouched beer and bustling suburban context. |
47
+ | img-46AC-B1CB-06-5 | google/gemma-3-4b-it | deadpan | 3 | 2 | 2 | Invents scent, knuckles, D&G neon; voice too lyrical. |
48
+ | img-46AC-B1CB-06-5 | google/gemma-3-4b-it | noir | 2 | 1 | 4 | Noir lands, but rain and falling ID card are invented. |
49
+ | img-46AC-B1CB-06-5 | google/gemma-3-4b-it | nature_doc | 4 | 3 | 2 | Good visible details; invents sweat and misses nature-documentary voice. |
50
+ | img-46AC-B1CB-06-5 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 4 | 2 | 4 | Flat and specific; invents masked cap-wearing man talking. |
51
+ | img-46AC-B1CB-06-5 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 3 | 3 | 3 | Some real pose details; rain and clothing colors are wrong. |
52
+ | img-46AC-B1CB-06-5 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 2 | 4 | 1 | Mostly grounded but generic; nature-documentary style barely appears. |
53
+ | img-46AC-B1CB-06-6 | google/gemma-3-4b-it | deadpan | 3 | 3 | 2 | Vent and blanket grounded; phone beam and dust motes invented. |
54
+ | img-46AC-B1CB-06-6 | google/gemma-3-4b-it | noir | 2 | 1 | 4 | Invents ceiling fan, paper, sheet, blue glow, and hum. |
55
+ | img-46AC-B1CB-06-6 | google/gemma-3-4b-it | nature_doc | 2 | 2 | 1 | Some bedding detail, but invents sneaker and lacks documentary voice. |
56
+ | img-46AC-B1CB-06-6 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 2 | 1 | 2 | Invents ceiling fans, posture, armrests, and vent state. |
57
+ | img-46AC-B1CB-06-6 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 2 | 2 | 4 | Noir lands, but cigarette box, rain, and small table are invented. |
58
+ | img-46AC-B1CB-06-6 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 3 | 3 | 2 | Wicker lamps and circular light grounded; posture and vent phrasing wrong. |
59
+ | img-46AC-B1CB-06-7 | google/gemma-3-4b-it | deadpan | 3 | 2 | 2 | Good backpack/cabinet details, but invents list, apple core, unseen events. |
60
+ | img-46AC-B1CB-06-7 | google/gemma-3-4b-it | noir | 3 | 2 | 4 | Noir lands, but invents key and misstates oven/cabinet relation. |
61
+ | img-46AC-B1CB-06-7 | google/gemma-3-4b-it | nature_doc | 3 | 2 | 1 | Invents bag contents and open oven; not nature-documentary style. |
62
+ | img-46AC-B1CB-06-7 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 3 | 3 | 2 | Mostly visible scene, but claims empty bag, key, departure speculation. |
63
+ | img-46AC-B1CB-06-7 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 2 | 1 | 3 | Invents cigarette, detective, breeze; only backpack/counter are grounded. |
64
+ | img-46AC-B1CB-06-7 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 2 | 4 | 1 | Mostly grounded but generic, with little actual nature-documentary voice. |
65
+ | img-46AC-B1CB-06-8 | google/gemma-3-4b-it | deadpan | 2 | 2 | 2 | Umbrella and man fit, but glass, newspaper, timing invented. |
66
+ | img-46AC-B1CB-06-8 | google/gemma-3-4b-it | noir | 2 | 1 | 4 | Noir tone works; glasses, lemon, briefcase, truck, exhaust hallucinated. |
67
+ | img-46AC-B1CB-06-8 | google/gemma-3-4b-it | nature_doc | 2 | 1 | 2 | Mentions umbrella and paving, but briefcase, bottle, cyclist invented. |
68
+ | img-46AC-B1CB-06-8 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 3 | 3 | 2 | Tables, chairs, umbrella, tree visible; reflection and smile invented. |
69
+ | img-46AC-B1CB-06-8 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 2 | 2 | 3 | Solitary figure and cigarette are wrong; mood partly noir. |
70
+ | img-46AC-B1CB-06-8 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 1 | 3 | 2 | Mostly vague, misses visible details; falsely implies man is alone. |
71
+ | img-46AC-B1CB-06-9 | google/gemma-3-4b-it | deadpan | 2 | 2 | 2 | Mostly invents hand, document, action; tone too literary. |
72
+ | img-46AC-B1CB-06-9 | google/gemma-3-4b-it | noir | 2 | 2 | 4 | Noir works, but invents hands, paper, dust, waiting. |
73
+ | img-46AC-B1CB-06-9 | google/gemma-3-4b-it | nature_doc | 3 | 2 | 2 | Good counter and staircase; invents bread flyer and inner state. |
74
+ | img-46AC-B1CB-06-9 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 3 | 3 | 3 | Braids and bowed head fit; yellow cushion is not visible. |
75
+ | img-46AC-B1CB-06-9 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 2 | 1 | 4 | Noir tone, but candle, papers, floorboards, detective are invented. |
76
+ | img-46AC-B1CB-06-9 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 2 | 2 | 1 | Invents sign text and cafe; little nature-documentary voice. |
docs/eval/run-005-scored.md ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Small Cuts — M1 Eval, Scored (run-005)
2
+
3
+ Judge: Codex (GPT-5 vision) scoring each narration against the actual photo.
4
+ Rubric: S(pecificity), G(roundedness), V(oice), 1-5. Pick rule: S>=4 and G>=4 on most images.
5
+
6
+ ## Verdict
7
+
8
+ | Model | mean S | mean G | mean V | cells S>=4 & G>=4 | images passing (>=2/3 styles) |
9
+ |---|---|---|---|---|---|
10
+ | Qwen/Qwen2.5-VL-3B-Instruct | 3.73 | 3.13 | 2.43 | 9/30 | 2/10 |
11
+ | Qwen/Qwen2.5-VL-7B-Instruct | 3.47 | 3.40 | 3.03 | 7/30 | 1/10 |
12
+ | google/gemma-3-4b-it | 3.33 | 2.70 | 2.63 | 5/30 | 1/10 |
13
+
14
+ ## Per-cell scores
15
+
16
+ | Image | Model | Style | S | G | V | Judge note |
17
+ |---|---|---|---|---|---|---|
18
+ | img-46AC-B1CB-06-0 | google/gemma-3-4b-it | deadpan | 4 | 3 | 5 | Good objects, but poster text and pot color are wrong. |
19
+ | img-46AC-B1CB-06-0 | google/gemma-3-4b-it | noir | 3 | 2 | 2 | TripAdvisor, pillow image, and poster text are invented. |
20
+ | img-46AC-B1CB-06-0 | google/gemma-3-4b-it | nature_doc | 3 | 2 | 1 | Key text and pot color wrong; no nature-documentary voice. |
21
+ | img-46AC-B1CB-06-0 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 5 | 4 | 5 | Strong specifics; digital sign and 2026 are questionable. |
22
+ | img-46AC-B1CB-06-0 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 3 | 3 | 2 | Real objects, but shelf placement is wrong and noir is thin. |
23
+ | img-46AC-B1CB-06-0 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 4 | 3 | 2 | Good text, but office/digital display are invented and style is weak. |
24
+ | img-46AC-B1CB-06-0 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 5 | 5 | 4 | Accurate specifics; slightly too expressive for deadpan. |
25
+ | img-46AC-B1CB-06-0 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 4 | 3 | 4 | Noir lands, but corkboard and long shadows are shaky. |
26
+ | img-46AC-B1CB-06-0 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 3 | 3 | 2 | Invents motion and lacks exact visible sign text. |
27
+ | img-46AC-B1CB-06-1 | google/gemma-3-4b-it | deadpan | 3 | 3 | 5 | Plain and factual, but sign text is wrong. |
28
+ | img-46AC-B1CB-06-1 | google/gemma-3-4b-it | noir | 4 | 3 | 2 | Good clothing/counter detail; wrong sign and weak noir. |
29
+ | img-46AC-B1CB-06-1 | google/gemma-3-4b-it | nature_doc | 4 | 2 | 1 | Wrong sign and blue screen; no nature-documentary voice. |
30
+ | img-46AC-B1CB-06-1 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 5 | 3 | 4 | Many specifics, but jeans, tied hair, laptop, waiting are unsupported. |
31
+ | img-46AC-B1CB-06-1 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 5 | 4 | 2 | Mostly grounded specifics, but little noir atmosphere. |
32
+ | img-46AC-B1CB-06-1 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 5 | 3 | 1 | Specific but monitor/sign details blur together; no documentary voice. |
33
+ | img-46AC-B1CB-06-1 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 4 | 4 | 4 | Mostly accurate, though man and desk activity are inferred. |
34
+ | img-46AC-B1CB-06-1 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 3 | 3 | 4 | Noir tone works; shadows and intrigue are embellished. |
35
+ | img-46AC-B1CB-06-1 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 2 | 2 | 2 | Mislabels store as office/workplace and stays generic. |
36
+ | img-46AC-B1CB-06-2 | google/gemma-3-4b-it | deadpan | 3 | 3 | 4 | White car is right; sign text appears invented. |
37
+ | img-46AC-B1CB-06-2 | google/gemma-3-4b-it | noir | 3 | 3 | 2 | Correct car and yellow line; sign text not visible. |
38
+ | img-46AC-B1CB-06-2 | google/gemma-3-4b-it | nature_doc | 2 | 2 | 1 | Sign text is garbled and style is not nature documentary. |
39
+ | img-46AC-B1CB-06-2 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 3 | 4 | 4 | Mostly accurate scene description, but red car is foreground, not parked. |
40
+ | img-46AC-B1CB-06-2 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 4 | 4 | 2 | Good visible specifics; noir voice is weak. |
41
+ | img-46AC-B1CB-06-2 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 4 | 5 | 1 | Grounded specifics, but no nature-documentary framing. |
42
+ | img-46AC-B1CB-06-2 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 4 | 5 | 4 | Accurate concrete details with a suitably flat voice. |
43
+ | img-46AC-B1CB-06-2 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 2 | 3 | 4 | Noir tone lands, but relies on invented mystery atmosphere. |
44
+ | img-46AC-B1CB-06-2 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 2 | 4 | 2 | Mostly grounded but generic and not documentary-like. |
45
+ | img-46AC-B1CB-06-3 | google/gemma-3-4b-it | deadpan | 4 | 4 | 5 | Concise and mostly accurate; wall location is slightly imprecise. |
46
+ | img-46AC-B1CB-06-3 | google/gemma-3-4b-it | noir | 4 | 3 | 2 | Invents sheet spilling onto floor; noir voice is weak. |
47
+ | img-46AC-B1CB-06-3 | google/gemma-3-4b-it | nature_doc | 5 | 3 | 1 | Good objects, but lantern glow and hanger are uncertain; not nature-doc. |
48
+ | img-46AC-B1CB-06-3 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 5 | 3 | 3 | Many specifics, but workspace, glow, and folded blanket are off. |
49
+ | img-46AC-B1CB-06-3 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 2 | 2 | 3 | Invents desk lamp, chair, and dim lighting. |
50
+ | img-46AC-B1CB-06-3 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 3 | 2 | 1 | Misreads bed as desk and room as clean office; not nature-doc. |
51
+ | img-46AC-B1CB-06-3 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 5 | 3 | 3 | Strong specifics, but wooden lamp, hook, and cool-room inference are unsupported. |
52
+ | img-46AC-B1CB-06-3 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 4 | 3 | 4 | Good noir mood, but glow and whispering shadows are invented. |
53
+ | img-46AC-B1CB-06-3 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 3 | 3 | 2 | Some grounded objects, but vent sound and dancing shadows are invented. |
54
+ | img-46AC-B1CB-06-4 | google/gemma-3-4b-it | deadpan | 2 | 1 | 4 | Invents pipe and Coca-Cola bottle; mostly generic. |
55
+ | img-46AC-B1CB-06-4 | google/gemma-3-4b-it | noir | 2 | 1 | 2 | Invents cigarette, smoke, and Coca-Cola bottle. |
56
+ | img-46AC-B1CB-06-4 | google/gemma-3-4b-it | nature_doc | 2 | 1 | 1 | Invents pipe and smoke; weak documentary voice. |
57
+ | img-46AC-B1CB-06-4 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 5 | 4 | 4 | Strong specifics; pink beer wording is questionable. |
58
+ | img-46AC-B1CB-06-4 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 5 | 4 | 1 | Grounded details, but almost no noir voice. |
59
+ | img-46AC-B1CB-06-4 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 4 | 3 | 2 | Misstates drinks; limited nature-documentary framing. |
60
+ | img-46AC-B1CB-06-4 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 5 | 3 | 2 | Mistakes napkin dispenser for bottle; adds interpretation. |
61
+ | img-46AC-B1CB-06-4 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 3 | 3 | 3 | Some noir tone, but invents note implication and bottle. |
62
+ | img-46AC-B1CB-06-4 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 4 | 3 | 1 | Park and Coca-Cola bottle are wrong; not documentary style. |
63
+ | img-46AC-B1CB-06-5 | google/gemma-3-4b-it | deadpan | 2 | 2 | 4 | Flat voice, but bench, awning, D&G, and hours are wrong. |
64
+ | img-46AC-B1CB-06-5 | google/gemma-3-4b-it | noir | 2 | 3 | 3 | Some real door details, but bench and D&G sign are false. |
65
+ | img-46AC-B1CB-06-5 | google/gemma-3-4b-it | nature_doc | 3 | 2 | 1 | Hours text is close, but bench and red D&G sign hallucinated. |
66
+ | img-46AC-B1CB-06-5 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 4 | 4 | 4 | Strong visible specifics; cap and inside-person claim are uncertain. |
67
+ | img-46AC-B1CB-06-5 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 3 | 2 | 3 | Wet street, held sign, and neon Yes You Can are false. |
68
+ | img-46AC-B1CB-06-5 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 4 | 3 | 1 | Good specifics, but Dog Friendly cap claim is wrong. |
69
+ | img-46AC-B1CB-06-5 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 3 | 3 | 4 | Mostly grounded, but walking individual and Spanish hours are unsupported. |
70
+ | img-46AC-B1CB-06-5 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 2 | 4 | 4 | Moody and mostly grounded, but low on concrete photo details. |
71
+ | img-46AC-B1CB-06-5 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 3 | 3 | 2 | Some grounded details, but bustling street and nature-doc voice are weak. |
72
+ | img-46AC-B1CB-06-6 | google/gemma-3-4b-it | deadpan | 4 | 4 | 5 | Mostly concrete and accurate; hand/directly-below details are shaky. |
73
+ | img-46AC-B1CB-06-6 | google/gemma-3-4b-it | noir | 3 | 3 | 3 | Some specifics, but blanket floor and gray doorway are questionable. |
74
+ | img-46AC-B1CB-06-6 | google/gemma-3-4b-it | nature_doc | 3 | 3 | 1 | Grounding slips on wall/floor/object; no documentary voice. |
75
+ | img-46AC-B1CB-06-6 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 3 | 2 | 2 | Invents floor lamp, footrest, bed sway; only partly deadpan. |
76
+ | img-46AC-B1CB-06-6 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 2 | 2 | 3 | Invents circular window and motion; noir mood is present. |
77
+ | img-46AC-B1CB-06-6 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 2 | 2 | 1 | Invents circular window and ajar door; lacks nature-documentary framing. |
78
+ | img-46AC-B1CB-06-6 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 3 | 3 | 4 | Accurate hand and lampshade, but person/table/checkered shirt are off. |
79
+ | img-46AC-B1CB-06-6 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 2 | 2 | 4 | No ceiling fan visible; mood fits noir better than facts. |
80
+ | img-46AC-B1CB-06-6 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 4 | 4 | 1 | Good visible-room details, but essentially no nature-documentary voice. |
81
+ | img-46AC-B1CB-06-7 | google/gemma-3-4b-it | deadpan | 5 | 4 | 5 | Strong specifics, but invents a key on the cabinet door. |
82
+ | img-46AC-B1CB-06-7 | google/gemma-3-4b-it | noir | 4 | 3 | 2 | Invents key and lock; voice is mostly plain description. |
83
+ | img-46AC-B1CB-06-7 | google/gemma-3-4b-it | nature_doc | 5 | 3 | 1 | Specific, but key claim is false and nature-doc voice absent. |
84
+ | img-46AC-B1CB-06-7 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 4 | 5 | 4 | Grounded cabinet and backpack details; slightly interpretive at the end. |
85
+ | img-46AC-B1CB-06-7 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 4 | 3 | 4 | Noir tone works, but invents a doorway-like threshold. |
86
+ | img-46AC-B1CB-06-7 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 3 | 2 | 1 | Invents open-plan office setting and misses nature-documentary framing. |
87
+ | img-46AC-B1CB-06-7 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 5 | 3 | 4 | Many real details, but falsely says the oven is open. |
88
+ | img-46AC-B1CB-06-7 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 4 | 5 | 4 | Grounded details with a competent moody noir atmosphere. |
89
+ | img-46AC-B1CB-06-7 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 5 | 4 | 1 | Mostly grounded kitchen details, but nature-documentary voice is missing. |
90
+ | img-46AC-B1CB-06-8 | google/gemma-3-4b-it | deadpan | 4 | 4 | 5 | Concrete and flat; likely one seated man, bin placement roughly right |
91
+ | img-46AC-B1CB-06-8 | google/gemma-3-4b-it | noir | 4 | 3 | 2 | Pale rectangle and single chair are questionable; weak noir voice |
92
+ | img-46AC-B1CB-06-8 | google/gemma-3-4b-it | nature_doc | 4 | 4 | 2 | Mostly accurate specifics, but barely nature-documentary narration |
93
+ | img-46AC-B1CB-06-8 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 3 | 2 | 2 | Invents logo text, pedestrians, sound, and man alone |
94
+ | img-46AC-B1CB-06-8 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 2 | 3 | 3 | Grounded broad scene, but invents solitude and inner reflection |
95
+ | img-46AC-B1CB-06-8 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 3 | 3 | 1 | Some accurate setting details, but invents thoughts and lacks documentary voice |
96
+ | img-46AC-B1CB-06-8 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 4 | 5 | 4 | Specific and grounded; relaxed urban setting is slightly interpretive |
97
+ | img-46AC-B1CB-06-8 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 4 | 3 | 4 | Good noir tone, but invents readable Cafe text and some shadows |
98
+ | img-46AC-B1CB-06-8 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 3 | 3 | 2 | Invents rustling soundtrack and sky detail; style is generic pastoral |
99
+ | img-46AC-B1CB-06-9 | google/gemma-3-4b-it | deadpan | 4 | 3 | 4 | Good desk, plant, staircase, box; invents hands/document and curved stairs. |
100
+ | img-46AC-B1CB-06-9 | google/gemma-3-4b-it | noir | 3 | 2 | 2 | Counter is right, but hands, brown plant, curved stairs are shaky. |
101
+ | img-46AC-B1CB-06-9 | google/gemma-3-4b-it | nature_doc | 2 | 2 | 1 | Wrong sweater color, hands/document, plant color; no nature-documentary voice. |
102
+ | img-46AC-B1CB-06-9 | Qwen/Qwen2.5-VL-3B-Instruct | deadpan | 5 | 4 | 3 | Strong visible details; mental-state inference and glass railing weaken grounding. |
103
+ | img-46AC-B1CB-06-9 | Qwen/Qwen2.5-VL-3B-Instruct | noir | 4 | 3 | 3 | Braids, sweater, chair work; desk lamp is invented. |
104
+ | img-46AC-B1CB-06-9 | Qwen/Qwen2.5-VL-3B-Instruct | nature_doc | 4 | 3 | 1 | Good person/table details; sign is mislocated and voice misses nature-doc style. |
105
+ | img-46AC-B1CB-06-9 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 3 | 4 | 3 | Mostly grounded but generic, with contemplative inference instead of flat deadpan. |
106
+ | img-46AC-B1CB-06-9 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 3 | 2 | 4 | Noir mood lands, but shadow, swaying braids, gaze are invented. |
107
+ | img-46AC-B1CB-06-9 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 3 | 4 | 1 | Grounded broad scene, but poetic contemplation is not nature-documentary narration. |
docs/eval/run-006-scored.md ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Small Cuts — M1 Eval, Scored (run-006, dual judge)
2
+
3
+ Head-to-head: run-005 provisional pick **Qwen2.5-VL-7B** vs challenger **Qwen3-VL-8B** (prompt v3, temp 0.3, 10 glasses photos × 3 styles).
4
+
5
+ Two independent judges, different model families, same rubric and photos:
6
+
7
+ - **Judge A — Codex** (GPT-5 vision), same judge as run-004/005.
8
+ - **Judge B — Antigravity** (Gemini 3.1 Pro vision), added to address the single-judge weakness of the earlier picks.
9
+
10
+ Rubric: S(pecificity), G(roundedness), V(oice), 1-5. Pick rule: S>=4 and G>=4 on most images (>=2 of 3 styles per image).
11
+
12
+ ## Verdict — both judges, same ranking
13
+
14
+ ### Judge A: Codex (GPT-5)
15
+
16
+ | Model | mean S | mean G | mean V | cells S>=4 & G>=4 | images passing |
17
+ |---|---|---|---|---|---|
18
+ | Qwen/Qwen2.5-VL-7B-Instruct | 3.10 | 3.53 | 3.03 | 4/30 | 0/10 |
19
+ | Qwen/Qwen3-VL-8B-Instruct | 4.33 | 3.83 | 4.20 | 19/30 | 7/10 |
20
+
21
+ ### Judge B: Antigravity (Gemini 3.1 Pro)
22
+
23
+ | Model | mean S | mean G | mean V | cells S>=4 & G>=4 | images passing |
24
+ |---|---|---|---|---|---|
25
+ | Qwen/Qwen2.5-VL-7B-Instruct | 3.20 | 3.07 | 3.57 | 1/30 | 0/10 |
26
+ | Qwen/Qwen3-VL-8B-Instruct | 4.77 | 4.33 | 4.60 | 26/30 | 9/10 |
27
+
28
+ **Final pick: `Qwen/Qwen3-VL-8B-Instruct`.** It passes the pick rule for both judges (7/10 and 9/10 images) while the run-005 provisional pick Qwen2.5-VL-7B passes 0/10 for both. The provisional pick is overturned; the two judged conclusions agree independently.
29
+
30
+ ## Inter-judge agreement
31
+
32
+ | Dim | MAE | exact | within ±1 | Pearson r | Codex mean | Gemini mean |
33
+ |---|---|---|---|---|---|---|
34
+ | S | 0.50 | 53% | 97% | 0.76 | 3.72 | 3.98 |
35
+ | G | 0.75 | 35% | 90% | 0.51 | 3.68 | 3.70 |
36
+ | V | 0.60 | 60% | 85% | 0.70 | 3.62 | 4.08 |
37
+
38
+ Cohen's kappa on the pass/fail decision per cell: **0.59** (moderate agreement). Gemini runs ~0.3-0.5 hotter on S and V; G means are nearly identical. Disagreements concentrate in V (style taste) — on factual grounding the judges typically flag the *same* errors and differ only in severity (see per-cell notes). The model ranking is unaffected by judge choice.
39
+
40
+ ## Per-cell scores (Codex / Gemini)
41
+
42
+ | Image | Model | Style | S | G | V | Codex note | Gemini note |
43
+ |---|---|---|---|---|---|---|---|
44
+ | img-46AC-B1CB-06-0 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 5/4 | 5/4 | 4/2 | Accurate visible details; slightly more polished than deadpan. | Flowery language like 'proudly displaying' breaks the requested deadpan style. |
45
+ | img-46AC-B1CB-06-0 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 3/3 | 4/4 | 4/2 | Grounded but misses specifics like camera, 8,6, plant, flyers. | The phrase 'beacon of trust' completely contradicts the cynical noir tone. |
46
+ | img-46AC-B1CB-06-0 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 4/4 | 3/3 | 2/2 | Map location and pins are uncertain; style not documentary. | Misses the 'humans as observed species' angle and hallucinates map pins marking POIs. |
47
+ | img-46AC-B1CB-06-0 | Qwen/Qwen3-VL-8B-Instruct | deadpan | 5/5 | 4/4 | 5/5 | Very specific; air conditioner brand appears misread. | Excellent specifics and deadpan tone; minor hallucination on AC brand name (ferroli). |
48
+ | img-46AC-B1CB-06-0 | Qwen/Qwen3-VL-8B-Instruct | noir | 4/5 | 2/4 | 5/5 | Strong noir, but invents a man and story. | Incredible hardboiled tone with a very perceptive observation about the artificial plant. |
49
+ | img-46AC-B1CB-06-0 | Qwen/Qwen3-VL-8B-Instruct | nature_doc | 5/5 | 4/5 | 4/5 | Detailed and mostly grounded; map place and lavender are uncertain. | Perfectly extracts and translates Catalan text while treating the room as a human habitat. |
50
+ | img-46AC-B1CB-06-1 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 4/4 | 4/3 | 4/5 | Good shop details; denim skirt and conversation are slightly off. | Misidentifies denim jacket tied at waist as a denim skirt. |
51
+ | img-46AC-B1CB-06-1 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 3/4 | 4/3 | 3/5 | Mostly grounded, but generic noir and denim skirt is inaccurate. | Misidentifies the denim jacket tied at waist as a denim skirt. |
52
+ | img-46AC-B1CB-06-1 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 2/3 | 3/4 | 3/5 | Open-plan office is wrong; few concrete photo specifics. | Mischaracterizes the retail repair shop as an open-plan office. |
53
+ | img-46AC-B1CB-06-1 | Qwen/Qwen3-VL-8B-Instruct | deadpan | 5/5 | 5/5 | 5/5 | Strong visible specifics: clothing, clerk, bag, accessories, extinguisher sign. | Perfectly identifies clothing layers, bag, and the EXTINTOR sign. |
54
+ | img-46AC-B1CB-06-1 | Qwen/Qwen3-VL-8B-Instruct | noir | 4/5 | 4/5 | 5/5 | Noir lands; typing and man are inferred beyond visible evidence. | Accurately captures pale yellow walls and the exact sign text. |
55
+ | img-46AC-B1CB-06-1 | Qwen/Qwen3-VL-8B-Instruct | nature_doc | 4/5 | 4/4 | 5/5 | Style works; blinking devices and bowed heads are not clearly visible. | Hallucinates packaged goods on the shelves as 'blinking devices'. |
56
+ | img-46AC-B1CB-06-2 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 3/4 | 3/3 | 5/5 | Sign text is inaccurate; otherwise grounded and flat. | Misreads PLACES as Plaça and inaccurately describes the red car's shadow. |
57
+ | img-46AC-B1CB-06-2 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 2/3 | 4/3 | 4/5 | Mostly grounded, but light on photo-specific details. | Inaccurately claims the red car casts a shadow over the sidewalk. |
58
+ | img-46AC-B1CB-06-2 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 2/2 | 4/5 | 2/2 | Grounded but generic; nature documentary voice barely appears. | Fails the nature doc prompt by lacking the humans as species observational framing. |
59
+ | img-46AC-B1CB-06-2 | Qwen/Qwen3-VL-8B-Instruct | deadpan | 5/5 | 5/4 | 5/5 | Strong visible specifics, including shadow, cars, sign, and facade. | Car is parked parallel, not angled; shadow shows both hands raised holding camera. |
60
+ | img-46AC-B1CB-06-2 | Qwen/Qwen3-VL-8B-Instruct | noir | 4/5 | 3/3 | 5/5 | Noir lands, but gender and movement are invented. | Hallucinates the brightly lit brick building as being a dark silhouette. |
61
+ | img-46AC-B1CB-06-2 | Qwen/Qwen3-VL-8B-Instruct | nature_doc | 4/5 | 4/5 | 3/4 | Specific and mostly grounded, but more poetic than documentary. | Accurate details like the dual-tone facade and barred windows; decent documentary tone. |
62
+ | img-46AC-B1CB-06-3 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 4/4 | 4/3 | 3/3 | Good objects; lamp casting light is uncertain and prose slightly decorative. | Ends too editorial for deadpan; hallucinates lamp is woven and lit. |
63
+ | img-46AC-B1CB-06-3 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 3/4 | 4/3 | 4/4 | Noir mood works; claims stay mostly visible but fairly generic. | Good noir mood but hallucinates an intricate, glowing lamp. |
64
+ | img-46AC-B1CB-06-3 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 3/4 | 4/3 | 1/2 | Mostly grounded, but lacks nature-documentary framing. | Fails nature doc voice; hallucinates unlit lamp casting shadows. |
65
+ | img-46AC-B1CB-06-3 | Qwen/Qwen3-VL-8B-Instruct | deadpan | 5/5 | 5/5 | 5/5 | Highly specific, accurate, and flatly observational. | Highly accurate specifics, correctly notes unlit lamp, perfect deadpan. |
66
+ | img-46AC-B1CB-06-3 | Qwen/Qwen3-VL-8B-Instruct | noir | 4/5 | 4/5 | 5/5 | Strong noir voice; face reflection wording slightly overstates. | Superb noir voice grounded perfectly in the actual visual elements. |
67
+ | img-46AC-B1CB-06-3 | Qwen/Qwen3-VL-8B-Instruct | nature_doc | 4/5 | 3/3 | 2/3 | Invents rings of light; more poetic than documentary. | Animates objects instead of humans; hallucinates concentric lamp light. |
68
+ | img-46AC-B1CB-06-4 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 4/4 | 3/2 | 4/4 | Good specifics, but invents Coca-Cola bottle and sipping. | Mistakes Coca-Cola napkin dispenser for a bottle and hallucinates sipping action. |
69
+ | img-46AC-B1CB-06-4 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 3/3 | 3/2 | 2/3 | Invents Coca-Cola bottle; noir voice is weak. | Hallucinates a Coca-Cola bottle; noir voice is somewhat generic. |
70
+ | img-46AC-B1CB-06-4 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 3/4 | 4/3 | 2/4 | Mostly grounded, but style is not documentary-like. | Misidentifies the glasses of beer as a bottle. |
71
+ | img-46AC-B1CB-06-4 | Qwen/Qwen3-VL-8B-Instruct | deadpan | 5/5 | 5/5 | 5/5 | Strong visible details and flat delivery. | Exceptional accuracy and specificity; perfectly maintains a deadpan tone. |
72
+ | img-46AC-B1CB-06-4 | Qwen/Qwen3-VL-8B-Instruct | noir | 5/5 | 3/4 | 4/5 | Strong voice, but beer counts/status and brand claim are wrong. | Great style but falsely claims the beer brand isn't in the frame. |
73
+ | img-46AC-B1CB-06-4 | Qwen/Qwen3-VL-8B-Instruct | nature_doc | 3/5 | 2/4 | 2/5 | Overwritten, invents motion and untouched drinks. | Invented head movement and falsely claimed the partially empty drinks were untouched. |
74
+ | img-46AC-B1CB-06-5 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 3/3 | 3/2 | 4/4 | Mostly visible setup, but cap and sunglasses appear invented. | Hallucinates cap and sunglasses on the reflected person, likely confusing it with the sticker. |
75
+ | img-46AC-B1CB-06-5 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 3/2 | 4/4 | 4/5 | Grounded scene with decent noir mood, limited concrete specifics. | Captures noir atmosphere well but relies on generic descriptions rather than specific image details. |
76
+ | img-46AC-B1CB-06-5 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 2/3 | 2/4 | 2/2 | Invents swaying leaves, breeze, and cool gray backdrop. | Fails the nature doc prompt, providing poetic observation instead of clinical species analysis. |
77
+ | img-46AC-B1CB-06-5 | Qwen/Qwen3-VL-8B-Instruct | deadpan | 5/5 | 5/5 | 5/5 | Strong visible specifics: sticker, QR code, pipes, hanging lights. | Flawless deadpan delivery with highly accurate, specific observations of sticker text and interior details. |
78
+ | img-46AC-B1CB-06-5 | Qwen/Qwen3-VL-8B-Instruct | noir | 4/5 | 3/5 | 5/5 | Noir lands, but infers language meaning and shadow drama. | Masterful noir voice that brilliantly integrates specific real-world details like the foreign posted hours. |
79
+ | img-46AC-B1CB-06-5 | Qwen/Qwen3-VL-8B-Instruct | nature_doc | 3/4 | 4/5 | 3/3 | Mostly grounded, but nature-documentary voice is weak and vague. | Good specifics, but the voice is too poetic and misses the detached clinical tone. |
80
+ | img-46AC-B1CB-06-6 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 3/4 | 3/2 | 3/3 | Good ceiling details, but invents seated person and table activity. | Hallucinates a person sitting at a table in the background; plaid is in foreground. |
81
+ | img-46AC-B1CB-06-6 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 2/2 | 4/4 | 4/4 | Mostly grounded, but sparse and ignores visible room specifics. | Captures the noir mood well but lacks specific details from the room. |
82
+ | img-46AC-B1CB-06-6 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 3/3 | 4/5 | 1/2 | Grounded room details, but not nature-documentary narration. | Fails to capture the nature documentary persona, sounding more like a generic description. |
83
+ | img-46AC-B1CB-06-6 | Qwen/Qwen3-VL-8B-Instruct | deadpan | 5/5 | 4/5 | 4/5 | Strong specifics; plaid shirt and pillow relation seem misread. | Excellent specific details and perfect, matter-of-fact deadpan delivery without hallucinations. |
84
+ | img-46AC-B1CB-06-6 | Qwen/Qwen3-VL-8B-Instruct | noir | 3/5 | 4/5 | 5/5 | Noir lands; concrete claims mostly check, with speculative gendered simile. | Evocative noir style grounded perfectly in the actual visual elements of the room. |
85
+ | img-46AC-B1CB-06-6 | Qwen/Qwen3-VL-8B-Instruct | nature_doc | 3/4 | 3/4 | 1/5 | Poetic, not nature-doc; overstates unseen subject and plural lanterns. | Good documentary tone, but incorrectly claims the subject is unseen when legs are visible. |
86
+ | img-46AC-B1CB-06-7 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 5/4 | 5/3 | 4/4 | Accurate visible details; slightly more polished than flat deadpan | Hallucinates straps hanging over the edge. |
87
+ | img-46AC-B1CB-06-7 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 3/3 | 2/1 | 3/5 | Invents ajar oven door and towel; noir only partly lands | Completely hallucinates an open oven and towel. |
88
+ | img-46AC-B1CB-06-7 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 2/2 | 4/4 | 1/2 | Mostly grounded but generic, not nature-documentary style | Reads like real estate copy, not nature documentary. |
89
+ | img-46AC-B1CB-06-7 | Qwen/Qwen3-VL-8B-Instruct | deadpan | 5/5 | 5/4 | 5/5 | Specific, accurate, and flatly observational | Accurate and deadpan, but misses the leaning shelf. |
90
+ | img-46AC-B1CB-06-7 | Qwen/Qwen3-VL-8B-Instruct | noir | 5/5 | 4/4 | 5/5 | Strong noir with real TEKA detail; leather is uncertain | Excellent noir style and spots the TEKA logo. |
91
+ | img-46AC-B1CB-06-7 | Qwen/Qwen3-VL-8B-Instruct | nature_doc | 4/4 | 4/4 | 2/2 | Grounded specifics, but poetic noir-adjacent rather than nature documentary | Tone is poetic and reverent, not nature documentary. |
92
+ | img-46AC-B1CB-06-8 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 4/4 | 3/2 | 4/4 | Good specifics, but men are not at separate tables. | Men are at the same table, not separate ones. |
93
+ | img-46AC-B1CB-06-8 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 3/3 | 4/2 | 3/4 | Mostly grounded, but noir voice is mild. | Hallucinates dappled shadows and motion blur. |
94
+ | img-46AC-B1CB-06-8 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 3/2 | 3/3 | 2/2 | Generic, invents breeze and whispering ambiance. | Fails the nature doc voice; just sounds peaceful. |
95
+ | img-46AC-B1CB-06-8 | Qwen/Qwen3-VL-8B-Instruct | deadpan | 5/5 | 4/3 | 5/5 | Strong visible details; passing cars are inferred. | Catches upside-down logo, but hallucinates dappled shadows. |
96
+ | img-46AC-B1CB-06-8 | Qwen/Qwen3-VL-8B-Instruct | noir | 3/3 | 2/2 | 5/5 | Strong noir, but invents drinks, roar, flickering streetlamp, damp pavement. | Hallucinates night tropes like yellow glow and damp pavement. |
97
+ | img-46AC-B1CB-06-8 | Qwen/Qwen3-VL-8B-Instruct | nature_doc | 4/4 | 4/5 | 4/4 | Grounded and observant, though more poetic than documentary. | Accurate details with a solid observational documentary voice. |
98
+ | img-46AC-B1CB-06-9 | Qwen/Qwen2.5-VL-7B-Instruct | deadpan | 4/3 | 3/2 | 3/4 | Good visible details, but invents readable Café sign. | Hallucinates 'Café' text on the sign. |
99
+ | img-46AC-B1CB-06-9 | Qwen/Qwen2.5-VL-7B-Instruct | noir | 3/2 | 4/4 | 4/5 | Grounded mood, but light on concrete photo specifics. | Generic details but captures the moody noir tone well. |
100
+ | img-46AC-B1CB-06-9 | Qwen/Qwen2.5-VL-7B-Instruct | nature_doc | 2/2 | 2/2 | 2/4 | Invents plant-tending and a glass ceiling. | Hallucinates a glass ceiling and tending to a plant. |
101
+ | img-46AC-B1CB-06-9 | Qwen/Qwen3-VL-8B-Instruct | deadpan | 5/5 | 5/5 | 5/5 | Accurate, specific, and flatly observational. | Extremely accurate details with a perfectly flat, observant tone. |
102
+ | img-46AC-B1CB-06-9 | Qwen/Qwen3-VL-8B-Instruct | noir | 5/4 | 3/4 | 5/5 | Strong noir, but invents dust, smells, and cigarette traces. | Strong noir voice with great specifics; minor atmospheric embellishments. |
103
+ | img-46AC-B1CB-06-9 | Qwen/Qwen3-VL-8B-Instruct | nature_doc | 5/5 | 4/5 | 2/2 | Specific and mostly grounded, but not nature-documentary voice. | Highly accurate details, but voice sounds more like architectural commentary. |
104
+
105
+ ## Method
106
+
107
+ Same loop as run-004/005 (docs/eval/run-004-scored.md), run twice: photos relayed from the Spark, HEIC→JPEG, one judge call per photo with all six narrations + rubric, strict-JSON S/G/V, aggregated offline. Judge B uses the identical prompt with the photo opened from disk instead of attached.
docs/field-notes-small-cuts.md ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Small Cuts: Building a narrator for the moments that just happened
2
+
3
+ Small Cuts started from a simple product feeling: what if ordinary first-person moments could be
4
+ given the treatment of a tiny film? Not a chatbot, not a camera filter, not a social caption after
5
+ the fact. A narrator in your ear, speaking one dry, cinematic line about the thing that just
6
+ happened, and leaving behind a short cut you can replay.
7
+
8
+ The result is a glasses-to-ear-to-Space loop. Ray-Ban Meta glasses capture first-person image
9
+ frames. An iPhone sends selected moments to a private Mac Studio engine. A small vision-language
10
+ model writes a grounded deadpan narration, Kokoro turns it into speech, the wearer hears it, and
11
+ the same finished POV cut appears in a Hugging Face Gradio Space with video, voice, captions, title,
12
+ and library thumbnail.
13
+
14
+ ## The product shape
15
+
16
+ The important decision was to keep the Space as the public product surface, not the capture path.
17
+ The Space is a little streaming theater: the current cut plays in a 9:16 stage, the voice-over is
18
+ the master clock, captions follow the audio, and the library shows real POV frames instead of
19
+ generic generated cards.
20
+
21
+ The glasses path stays private. During the live demo, the iPhone writes to the local engine over
22
+ Tailnet. Public viewers and the Space only read finished scenes through a Cloudflare read gate:
23
+ `GET /v1/scenes`, `GET /v1/scenes/stream`, and `/media/*`. The public hostname blocks the
24
+ WebSocket capture path and visibility writes. That split is less glamorous than making everything
25
+ public, but it is the right product boundary: private capture, public replay. For the hackathon
26
+ demo this rides on a Cloudflare tunnel; a production version would turn that into durable auth,
27
+ domain, and publishing controls rather than opening the capture path.
28
+
29
+ ## Why small models worked
30
+
31
+ The model does not need to understand a whole movie. It needs to make one grounded observation from
32
+ a selected frame and say it with taste. The current narrator uses Qwen3-VL-8B through llama.cpp for
33
+ the live local path, with a prompt that is intentionally strict: only visible or directly inferable
34
+ facts, no invented objects, no fake sounds, no unsupported drama. Kokoro handles the voice.
35
+
36
+ That constraint became the voice of the product. Small Cuts is not trying to explain the world. It
37
+ is trying to notice the one detail that makes a moment feel like a scene.
38
+
39
+ ## What was harder than expected
40
+
41
+ The first hard part was synchronization. The native Gradio audio component is useful plumbing, but
42
+ it cannot be treated as a whole custom player. The working solution was to make the browser's audio
43
+ element the single playback clock and have the muted video, captions, and progress bar follow it.
44
+ That also avoids pretending that browsers allow audible autoplay. The cut starts when the viewer
45
+ clicks play.
46
+
47
+ The second hard part was making the library feel alive. Title cards are cinematic, but a grid of
48
+ title cards makes the product look like generated slides. Real POV thumbnails make it feel like a
49
+ channel of lived moments. The engine now stores a selected key frame as `frame.jpg`, writes a short
50
+ browser-playable `clip.mp4`, and serves both through the same media contract as the voice.
51
+
52
+ The third hard part was latency and resilience. Scene audio must reach the wearer before the viewer
53
+ storage work can matter. The engine sends the `SceneAudio` frame first, then stores the scene media
54
+ for the Space. Supplemental frames for the viewer clip are useful, but they must never block the
55
+ in-ear experience.
56
+
57
+ ## What the public Space shows
58
+
59
+ The Space currently opens on a five-cut generated library of real first-person glasses moments.
60
+ Each cut was sent through the regular WebSocket path with one selected mark frame plus a short
61
+ POV buffer:
62
+
63
+ - `A hand, palm up, blocks the camera's view, revealing a...`
64
+ - `The camera lingers on a concrete alleyway at night, the...`
65
+ - `The car door is open, revealing a black interior and a...`
66
+ - `The driver grips the steering wheel with both hands, thumbs...`
67
+ - `The yellow circle with numbers is painted on the floor...`
68
+
69
+ Those cuts make the channel non-empty for first-time visitors without using human-written
70
+ narration as the product sample. The live demo path uses the same public read surface: when a new
71
+ local engine scene is created, the Space polls the read gate, fetches the media files, and replays
72
+ the just-happened cut with synced video, audio, captions, and title.
73
+
74
+ ## What is v1, and what comes next
75
+
76
+ This version sends image frames, not source audio. That is intentional for the hackathon build:
77
+ source sound is not needed for the narrator concept, and excluding it keeps privacy and contracts
78
+ simpler.
79
+
80
+ The current live model reasons over sampled frames from an intentional Action-to-Cut take. The
81
+ viewer can show a short POV clip assembled from those supplemental frames, but full video reasoning
82
+ is a v2 direction. The next version should buffer a low-resolution clip directly into the Space,
83
+ generate narration fragments with timestamps, and let the finished cut feel even closer to what the
84
+ wearer experienced.
85
+
86
+ Title generation now shares the model call with narration: the model returns a short grounded title
87
+ and the spoken narration together, with deterministic title derivation kept as a fallback for
88
+ malformed output. A later contract can add caption timing and confidence in the same structured
89
+ response.
90
+
91
+ ## Why this matters
92
+
93
+ The best version of Small Cuts is not a camera feature. It is a private narrator that notices the
94
+ little cinematic beats a person usually walks past, says one honest line in the ear, and leaves
95
+ behind a tiny film that feels like it happened on purpose.
96
+
97
+ That is the small adventure: not making the model bigger, but making the moment feel seen.
docs/hackathon-rules.md ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build Small Hackathon — Rules Summary (verified 2026-06-12)
2
+
3
+ Facts verified from primary sources:
4
+
5
+ - **Field Guide** (official): https://huggingface.co/spaces/build-small-hackathon/field-guide
6
+ — read raw `details.md`, `faq.md`, `partner-resources.md`, and the site source
7
+ (`src/lib/data/content.ts`, `src/lib/readme.ts`) on 2026-06-12.
8
+ - Organizer message in Discord (relayed by Carlos in issue #22, 2026-06-12).
9
+ - Registration emails from `yuvraj@huggingface.co` (2026-06-03/04). Carlos **is registered**.
10
+ - Official org: https://huggingface.co/build-small-hackathon (~1,916 members, 365 spaces on 2026-06-12).
11
+
12
+ ## Identity
13
+
14
+ - **Name:** Build Small Hackathon — "Small Models, Big Adventures"
15
+ - **Hosts:** Gradio · Hugging Face. Sponsors: OpenAI, NVIDIA, OpenBMB, Cohere, BFL, Modal, JetBrains.
16
+ - **Hack window:** June 5 – June 15, 2026.
17
+ - **Deadline (CONFIRMED, organizer in Discord): June 15, 23:59 UTC.**
18
+ The Space must be **public** and **moved under the org** by then — both have been
19
+ forgotten by participants in past events; ours is already created (see below).
20
+
21
+ ## Hard constraints
22
+
23
+ 1. **Model size:** every individual model **under 32B total parameters** (combinations fine;
24
+ each model's *total* — not active — count is what matters).
25
+ 2. **Platform:** Gradio app hosted as a Space under `build-small-hackathon/` (Docker allowed
26
+ only if the interface is still a Gradio Space).
27
+ 3. **Demo video:** required — judges must be able to evaluate even if GPU/API limits block a
28
+ live run. Upload to YouTube, as a file in the Space, or anywhere public.
29
+ 4. **Social post:** required — one post showcasing the app, **linked from the Space README**.
30
+ 5. **ZeroGPU limit:** max 10 ZeroGPU apps per user (we use 1).
31
+
32
+ ## Submission mechanics (CONFIRMED)
33
+
34
+ Tracks/badges are claimed via **namespaced tags in the Space README frontmatter**
35
+ (`src/lib/readme.ts` is the official parser):
36
+
37
+ ```yaml
38
+ tags:
39
+ - track:wood # or track:backyard
40
+ - sponsor:openai # sponsor prizes you're entering
41
+ - achievement:offgrid # self-declared badges, verified at judging
42
+ ```
43
+
44
+ Checklist from the Field Guide "submitting" page:
45
+
46
+ - [x] Join the org (macayaven is a member)
47
+ - [x] Space exists under the org: https://huggingface.co/spaces/build-small-hackathon/small-cuts
48
+ (public, RUNNING placeholder since 2026-06-12; real app lands in M3, #14)
49
+ - [ ] Upload the real app (M3)
50
+ - [ ] Demo video — public link (M4)
51
+ - [ ] Social post — link it in the Space README (M4)
52
+ - [ ] README: track/achievement tags + short write-up of idea, how it was built, tech used
53
+ - [ ] Run the **submission analyzer** before the deadline (how-to: youtube.com/watch?v=1iR65sq0HmA)
54
+
55
+ ## Tracks
56
+
57
+ - **Backyard AI** (`track:backyard`) — practical, problem-solving, daily-life apps.
58
+ - **Thousand Token Wood** (`track:wood`) — whimsical, delightful, AI-native fun.
59
+
60
+ **Small Cuts targets `track:wood`** (tag already on the placeholder Space).
61
+
62
+ ## Bonus achievements (self-declared tags, no prize, verified at judging)
63
+
64
+ | id | name | our status |
65
+ |---|---|---|
66
+ | `offgrid` | Off the Grid — no cloud APIs at runtime | ✅ architecture complies (declared) |
67
+ | `offbrand` | Off-Brand — custom UI past default Gradio | ✅ custom theme landed in #13 (declared) |
68
+ | `llama` | Llama Champion — llama.cpp runtime | ⏳ pending M3 LlamaCppBackend |
69
+ | `fieldnotes` | Field Notes — blog post / write-up | ⏳ draft created: hf.co/blog/macayaven/small-cuts-field-notes |
70
+ | `sharing` | Sharing is Caring — agent trace on the Hub | 💡 optional: publish the dual-judge eval artifacts as a dataset |
71
+ | `welltuned` | Well-Tuned — published fine-tune | ❌ not planned pre-deadline |
72
+
73
+ These also feed **Bonus Quest Champion** ($2,000 — most bonus criteria met).
74
+
75
+ ## Prized bonus badges
76
+
77
+ - Off-Brand $1,500 (judged; "gr.Server is your friend" — bar is higher than a theme)
78
+ - **Tiny Titan $1,500 — models must be ≤ 4B. We are NOT eligible (8B narrator).**
79
+ - Best Demo $1,000 · Best Agent $1,000 · Bonus Quest Champion $2,000 · Judges' Wildcard $1,000
80
+
81
+ ## Sponsor prizes relevant to us
82
+
83
+ - **OpenAI ($10k pool):** requires **Codex-attributed commits** in the Space history or a
84
+ linked GitHub repo. We genuinely use Codex for implementation but commits are not
85
+ Codex-attributed. If we enter `sponsor:openai`: add Codex attribution to future commits
86
+ and describe the Codex workflow in the README.
87
+ - **Modal ($20k credits prize):** requires using Modal for **development or runtime** + a
88
+ README note. FAQ explicitly confirms development-side Modal use (e.g. training/eval)
89
+ **stacks with `offgrid`** — offgrid is about the app's runtime only.
90
+ - OpenBMB (MiniCPM) / NVIDIA (Nemotron): different model families — not pursuing.
91
+
92
+ ## Modal credits (participant perk — answered 2026-06-12)
93
+
94
+ Carlos holds **$250 Modal credits** from registration (plus $20 HF credits, expire
95
+ 2026-07-31). **No Modal usage is planned in the current architecture**: dev/eval runs on
96
+ the Spark + Mac Studio, runtime is ZeroGPU. Using Modal would only matter for the Modal
97
+ prize (development-side use is rule-compatible with offgrid), but with a 3-day runway the
98
+ recommendation is: ship M3–M5 first; only port an eval/fine-tune job to Modal if time
99
+ remains. Redemption status of the credit code: Carlos to confirm.
100
+
101
+ ## Implications for architecture (unchanged, re-verified)
102
+
103
+ - The **Space is the judged artifact** — Mac Studio / Spark / Tailnet are dev-only.
104
+ - `offgrid` + `llama` ⇒ all-local inference inside the Space (ZeroGPU + llama.cpp CPU fallback).
105
+ - Ray-Ban Meta glasses are a capture device for the demo video, not runtime infra.
106
+ - ZeroGPU is shared: graceful CPU fallback + short inference. 40 min/day quota as org member.
107
+
108
+ ## Open questions
109
+
110
+ 1. Team rules (Topi/nojose registration status) — only matters if co-credit is wanted.
111
+ 2. Modal credit code redemption status — Carlos.
docs/implementation-plan.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Implementation Plan (deadline: June 15, 2026)
2
+
3
+ ## M0 — Bootstrap ✅ (June 11, this session)
4
+
5
+ - Repo scaffolding, CI (ruff + pytest + gitleaks), docs, KB notes.
6
+ - Vertical slice: Gradio app runs end-to-end with the **mock backend**
7
+ (image → director style → deterministic narration → title card).
8
+ - Pluggable backend interface with `transformers` and `llama_cpp` stubs wired.
9
+
10
+ ## M1 — Real narration (June 12)
11
+
12
+ - Eval harness ✅ (`src/small_cuts/eval.py`): fixed set of ~12 personal photos ×
13
+ candidate VLMs (SmolVLM2-2.2B, Qwen2.5-VL-3B/7B, gemma-3-4b-it) × 3 styles.
14
+ **Run on DGX Spark** (CUDA — results transfer to ZeroGPU; 128GB fits all
15
+ candidates without juggling):
16
+ ```bash
17
+ git clone https://github.com/macayaven/small-cuts && cd small-cuts
18
+ uv sync --extra local
19
+ uv run python -m small_cuts.eval --images ~/eval-photos --out eval-report.md
20
+ ```
21
+ Score the report for specificity/groundedness/voice (rubric included).
22
+ - Pick the model; tune the system prompt + per-style few-shots.
23
+ - Acceptance: 8/12 eval images produce narration a human laughs or nods at,
24
+ zero hallucinated objects in 10/12.
25
+
26
+ ## M2 — Performance layer (June 13)
27
+
28
+ - TTS (Kokoro-82M first candidate) behind the same backend pattern.
29
+ - Title-card renderer (PIL) + custom Gradio theme/CSS → **Off-Brand** quest.
30
+ - Streaming text reveal in UI.
31
+
32
+ ## M3 — Space live (June 13–14)
33
+
34
+ - Create `build-small-hackathon/small-cuts` Space; ZeroGPU + CPU fallback.
35
+ - Decide llama.cpp path (Llama Champion) based on VLM GGUF stability.
36
+ - Load test: 3 concurrent narrations; cold-start measurement.
37
+
38
+ ## M3.5 — Continuous Live Mode (stretch, only if M0–M3 green)
39
+
40
+ - Phone camera → Gradio streaming → scene-change gate → narration → streamed TTS
41
+ → played through Ray-Ban Metas via Bluetooth (audio path itself is free once
42
+ M2 TTS exists — use it in the demo video regardless).
43
+
44
+ ## M4 — Submission assets (June 14)
45
+
46
+ - Film POV footage with Ray-Ban Metas; cut ≤90s demo video on Mac Studio.
47
+ - Social post draft; Field Notes blog post draft (→ **Field Notes** quest).
48
+ - Stretch only if green: clip input, day-reel, LoRA fine-tune (**Well-Tuned**).
49
+
50
+ ## M5 — Submit (June 15, morning — do not use the deadline day as buffer-free)
51
+
52
+ - Final smoke test from both phones; submit Space + video + post.
53
+ - Verify quest claiming mechanism and apply tags/metadata.
docs/product-strategy.md ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Small Cuts — Product Strategy
2
+
3
+ ## Essence of the experience
4
+
5
+ In *The Invention of Lying*, the world is narrated by people who can only say
6
+ what is true — and the comedy and tenderness come from hearing ordinary moments
7
+ described with omniscient, cinematic certainty. **Small Cuts gives you that
8
+ narrator.** You show it a moment of your life; it tells you what kind of scene
9
+ you are actually in.
10
+
11
+ The emotional target is the gap between how a moment *feels* from inside and how
12
+ it *reads* from outside: a narrator that is observant, specific, a little too
13
+ honest, and unmistakably cinematic.
14
+
15
+ ## How the inspiration translates into UX
16
+
17
+ 1. **You bring a moment** — a photo or short clip from your phone, webcam, or
18
+ Ray-Ban Meta glasses footage.
19
+ 2. **You choose a director** — narration styles are *directors' cuts*: Noir
20
+ Detective, Nature Documentary, Trailer Voice, Deadpan Omniscient
21
+ (the *Invention of Lying* default), Telenovela, Wes Anderson Symmetrist…
22
+ 3. **The narrator speaks** — a small VLM looks at the scene and writes 2–4
23
+ sentences of narration grounded in what is actually visible; a small TTS
24
+ model performs it; the app renders a title card so the result is shareable.
25
+ 4. **One tap to re-cut** — same moment, different director. The re-cut is the
26
+ delight loop and the social loop ("show a friend" is literally the Track 2
27
+ judging criterion).
28
+
29
+ ## What the Director's Cut trajectory gets right
30
+
31
+ - The core concept (cinematic POV narration of lived moments) is original and
32
+ emotionally strong — worth keeping intact.
33
+ - The pivot toward Gradio (`wearables-gradio-poc`) correctly read the hackathon
34
+ rules: the judged artifact must be a Gradio Space.
35
+ - The local-model option (Qwen3-Omni) was prescient: it is exactly what the
36
+ "Off the Grid" quest rewards.
37
+
38
+ ## What it gets wrong / under-exploits (unverified where noted)
39
+
40
+ - **iOS companion app is unjudgeable.** Judges run a Space; a Swift app for
41
+ Ray-Ban Display can't be evaluated and consumes the remaining 4 days.
42
+ - **Gemini Live (cloud) breaks the rules' spirit and the best quests.**
43
+ Cloud narration forfeits "Off the Grid" and weakens the small-model story.
44
+ - **Glasses-as-infrastructure is fragile.** Device pairing on stage/video is a
45
+ demo killer; glasses should be a *capture source*, not a dependency.
46
+ - (Unverified — peer notes unreachable from this environment: the exact state
47
+ of `wearables-gradio-poc` may already address some of this.)
48
+
49
+ ## Improved concept: why Small Cuts can beat the original
50
+
51
+ - **Zero-friction for judges:** open the Space, drop any photo, get narrated.
52
+ No hardware, no account, no pairing.
53
+ - **Rule-maximal:** ≤32B local models inside the Space → eligible for Off the
54
+ Grid, Llama Champion, Tiny Titan, Off-Brand, Field Notes on top of Track 2.
55
+ Quests are scoring multipliers; the original trajectory could claim almost none.
56
+ - **The glasses still star** — in the demo video: Carlos walks through Madrid
57
+ wearing Ray-Ban Metas, the footage flows into the Space, and his day comes
58
+ back narrated. Emotional wow preserved, fragility removed.
59
+ - **Small-model fit is honest:** scene → short narration is a task a 2–8B VLM
60
+ genuinely does well; nobody needs GPT-class weights to be wry about your coffee.
61
+
62
+ ## Core demo flow (the video, ≤90s)
63
+
64
+ 1. POV clip from Ray-Ban Metas: ordinary morning moment (coffee, street, desk).
65
+ 2. Cut to the Space: frame dropped in, "Deadpan Omniscient" selected.
66
+ 3. Narration appears word-by-word and is *spoken*: specific, true, funny.
67
+ 4. Re-cut as "Noir" → same moment, new genre. Show a second moment.
68
+ 5. Closing card: "Every model under 32B. Everything runs in this Space."
69
+
70
+ ## Minimum lovable submission (must ship by June 14)
71
+
72
+ - Space under `build-small-hackathon/` org: image → styled narration → TTS audio
73
+ → shareable title card. 6 director styles. Custom cinematic theme.
74
+ - Local VLM (≈2–8B) + local TTS, ZeroGPU with CPU fallback.
75
+ - Demo video + social post + README.
76
+
77
+ ## Live Mode — narration in your ears, on the glasses (confirmed direction 2026-06-11)
78
+
79
+ Carlos confirmed the strategy with one addition: if technically feasible, the
80
+ narration should reach the wearer **in real time through the glasses**. Two
81
+ layers, in order of cost:
82
+
83
+ 1. **Audio out through the glasses — nearly free.** Ray-Ban Metas pair to the
84
+ phone as Bluetooth audio. The Space's TTS, played in the phone browser,
85
+ plays through the glasses' open-ear speakers with zero extra code. The demo
86
+ video can honestly show "hearing your life narrated in the glasses" the
87
+ moment M2 (TTS) lands.
88
+ 2. **Continuous Live Mode — the real work (stretch, M3.5).** Phone camera
89
+ streams frames into the Space (Gradio streaming/WebRTC); a scene-change
90
+ detector samples a frame every few seconds and narrates only when the scene
91
+ meaningfully changes (otherwise the narrator babbles); TTS streams back and
92
+ plays through the glasses. Needs rate limiting to respect ZeroGPU quotas.
93
+ 3. **Glasses-camera capture (post-hackathon).** Real-time access to the
94
+ glasses' own camera requires the Meta Wearables Device Access Toolkit
95
+ (developer preview, iOS) — the original `directors-cut` territory.
96
+ Feasibility/timeline **unverified**; out of scope before June 15.
97
+
98
+ ## Stretch (only after MLS is live)
99
+
100
+ - Continuous Live Mode (see above) — the highest-magic stretch.
101
+ - Short-clip input (sample frames → narrate the sequence as a scene).
102
+ - "Day reel": multiple moments stitched into one narrated cut.
103
+ - **Well-Tuned:** LoRA fine-tune of the narrator voice on a style corpus,
104
+ published on the Hub.
105
+ - llama.cpp runtime path for the Llama Champion badge (if VLM+GGUF is stable).
106
+
107
+ ## Riskiest assumptions
108
+
109
+ 1. A ≤8B VLM produces *specific, grounded* narration (not generic captions) —
110
+ prompt + few-shot needs early validation.
111
+ 2. ZeroGPU latency stays demo-acceptable under load; CPU fallback is tolerable.
112
+ 3. TTS quality carries the "performed narrator" feeling at small size.
113
+ 4. Space-under-org submission mechanics are as simple as they look (unverified).
114
+
115
+ ## First implementation milestones
116
+
117
+ | # | Milestone | Target |
118
+ |---|---|---|
119
+ | M0 | Repo bootstrap: CI, lint, tests, docs, mock-backend Gradio app runs | **done in this session** |
120
+ | M1 | Real VLM narration end-to-end locally (eval 2–3 candidate models, pick one) | June 12 |
121
+ | M2 | TTS + title card + custom theme (Off-Brand) | June 13 |
122
+ | M3 | Space live under org (ZeroGPU + CPU fallback), llama.cpp path decision | June 13–14 |
123
+ | M4 | Demo video filmed/cut, social post, Field Notes blog draft | June 14 |
124
+ | M5 | Submit + buffer day | June 15 |
docs/product/architecture.md ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Small Cuts — Product Architecture (the real-time loop)
2
+
3
+ > Scope: the **product** (post-hackathon #29). The hackathon Space remains
4
+ > described by [docs/architecture.md](../architecture.md) — it is the public
5
+ > demo shell of this architecture, with uploads standing in for the live
6
+ > glasses stream.
7
+
8
+ ## Current state (2026-06-12)
9
+
10
+ Batch pipeline, fully working: photo/clip upload → frame pick → Qwen3-VL-8B
11
+ narration (ZeroGPU or llama.cpp) → title card + on-demand Kokoro TTS. Live on
12
+ the hackathon Space. **No streaming, no session memory, no library, no mobile
13
+ app yet** — those are exactly what the three teams build.
14
+
15
+ ## Components & teams
16
+
17
+ | Component | Owner | Optimization goal |
18
+ |---|---|---|
19
+ | **Capture apps** (iOS/Android + Meta Wearables DAT) | Team Mobile | minimize signal sent, maximize moment quality, real-time delivery |
20
+ | **Narration engine** (home node: llama.cpp + Kokoro + session context) | Team Inference | prompt + session context + frames → Wes-Anderson-grade voice-over; max quality, min latency |
21
+ | **Viewer platform** (Gradio app: live view + library) + hackathon Space | Team Space | feel like a reference live-streaming platform (#28) |
22
+
23
+ Coordination: contracts below are the only coupling. Each team optimizes
24
+ freely inside its boundary; **contract changes are PRs labeled
25
+ `contract-change` and require orchestrator review**.
26
+
27
+ ## The temporal diagram
28
+
29
+ One narrated moment, end to end ("scene-gated discrete pipeline", see D1):
30
+
31
+ ```mermaid
32
+ sequenceDiagram
33
+ autonumber
34
+ participant G as Glasses (DAT)
35
+ participant M as Mobile app
36
+ participant E as Engine (home node)
37
+ participant V as Viewer (Gradio)
38
+ participant L as Library
39
+
40
+ Note over G,M: continuous camera session (DAT stream)
41
+ G->>M: video frames (SDK callback)
42
+ M->>M: scene gate + frame select + downscale ≤1024px
43
+ M->>E: MomentEnvelope (WS) — frames + context metadata
44
+ E->>E: session context → prompt build
45
+ E->>E: narrate (llama.cpp, warm 2–4.5s)
46
+ E->>E: TTS (Kokoro, ~3.5s)
47
+ E-->>M: SceneAudio (WS) — voice-over
48
+ M->>G: play in ear (BT speakers)
49
+ E-->>V: NarratedScene event (SSE) — live view
50
+ E->>L: append NarratedScene (visibility: private)
51
+ Note over V,L: user revisits in library → share / make public / go public-live
52
+ ```
53
+
54
+ **Latency budget v1** (gate fires → voice in ear), measured numbers in
55
+ parentheses:
56
+
57
+ | Stage | Budget |
58
+ |---|---|
59
+ | Phone: gate + select + encode + send | ≤ 1.0 s |
60
+ | Engine: narration, warm llama.cpp on Metal | ≤ 4.5 s (measured 2.0–4.2 s) |
61
+ | Engine: TTS, warm Kokoro CPU | ≤ 4.0 s (measured 3.6 s) |
62
+ | Return + playback start | ≤ 0.5 s |
63
+ | **End-to-end v1, warm p50 target** | **≤ 10 s** |
64
+ | End-to-end p90 (queueing, cold worker, retries) | ≤ 15 s |
65
+ | Stretch (sentence-pipelined narration→TTS, streamed audio) | ≤ 6 s |
66
+
67
+ Honesty notes (from adversarial review): the phone-side stage and queueing
68
+ are UNMEASURED — Team Mobile's first deliverable includes real upload
69
+ numbers, and `sent_at`/`latency_ms` exist in the contracts precisely to
70
+ attribute every millisecond. **A ~10 s voice is retrospective by nature**:
71
+ the narrator comments on the moment just past — the deadpan-omniscient
72
+ voice is written for exactly that register, the gate keeps moments sparse,
73
+ and the ≤ 6 s stretch (a future MAJOR contract version with real chunk
74
+ semantics — not a v1 toggle) closes the rest. Streamed audio is a
75
+ cross-team feature: Mobile must build buffered playback for it; it goes
76
+ through contract-change review, not a silent engine-side switch.
77
+
78
+ ## Decisions (status: adopted unless challenged via PR)
79
+
80
+ - **D1 — Unit of work is a gated Moment, not continuous video.** The phone
81
+ runs the cheap signal processing (scene-change gate — #15's M3.5 work
82
+ becomes mobile-side; frame selection; downscale to ≤1024px longest side,
83
+ matching the verified Qwen-VL vision-token constraint) and emits discrete
84
+ `MomentEnvelope`s. Rationale: bandwidth (the "optimize signal sent" goal),
85
+ battery, and the validated single-frame narration strategy. Continuous
86
+ streaming later = same contract at higher envelope frequency.
87
+ - **D2 — Transport is one bidirectional WebSocket per session** (phone ↔
88
+ engine, over the tailnet): envelopes up, audio + acks down. WebRTC only
89
+ becomes interesting if D1 flips to raw media. Team Mobile may challenge
90
+ with measurements.
91
+ - **D3 — The engine lives on the home node (Mac Studio), llama.cpp-first.**
92
+ Measured: warm narration 2.0–4.2 s on Metal — faster than ZeroGPU warm.
93
+ The HF Space is the public demo shell, not the product engine.
94
+ - **D4 — Audio returns as one complete clip per scene (v1).** Kokoro clips
95
+ are 5–15 s; streaming TTS buys ~2 s at real complexity cost AND requires
96
+ mobile-side buffered playback — so it is a future MAJOR contract version
97
+ decided cross-team, not an engine-side toggle.
98
+ - **D5 — Session context lives engine-side; its v1 scope is the clip.** The
99
+ phone sends only moment metadata (time, optional location label, optional
100
+ user hint); the engine owns the context and prompt construction (Mobile
101
+ never sees prompts; Inference never sees raw streams). **Within a clip the
102
+ narration is built chunk-by-chunk** — each moment narrated in sequence, every
103
+ chunk carrying the chronological context of the clip's earlier chunks, so the
104
+ clip reads as one coherent, non-repeating little story rather than a string of
105
+ disconnected captions. The cadence is **human-digestible**: each chunk is short
106
+ enough to land as *recent past* (what just happened) — never racing ahead to
107
+ narrate a future event — at a pace a listener can follow and retain, and that
108
+ the viewer's subtitles mirror line-for-line. Cross-clip / day-timeline memory
109
+ (→ #20) is a later extension; v1 is **intra-clip only** (no cross-clip memory).
110
+ - **D6 — Library is engine-side storage** (filesystem media + sqlite index),
111
+ entries are `NarratedScene`s with visibility `private | shared | public`
112
+ (default private). The viewer reads; it never writes media.
113
+ - **D7 — Viewer subscribes over SSE** to the engine's scene stream; library
114
+ entries and live events share the same `NarratedScene` schema (`seq` +
115
+ `Last-Event-ID` for resume). The viewer's only write is visibility
116
+ metadata via `PATCH /v1/scenes/{id}` — never media. "Public live" later =
117
+ the same stream behind an auth/visibility check; `owner`/`share_token`
118
+ are reserved in the schema now (v1 engines are single-user on the
119
+ tailnet).
120
+ - **D8 — Backpressure: queue depth ≤ 1, coalesce-to-newest.** The engine
121
+ acks every envelope at admission (`accepted | duplicate | rejected |
122
+ dropped_coalesced`), emits busy/ready status frames, and replaces a
123
+ queued un-started moment with a newer arrival. The phone suppresses its
124
+ gate while the engine is busy. Narrating stale moments is worse than
125
+ skipping them.
126
+ - **D9 — Playback freshness.** `SceneAudio` carries `play_by`
127
+ (default created + 60 s); the app never overlaps clips and drops any
128
+ clip whose deadline passed before playback started. Failures arrive as
129
+ first-class error frames (stage, code, retryable) on both the session
130
+ socket and the viewer stream — consumers render an honest timeline, not
131
+ just the success path.
132
+
133
+ ## Contracts
134
+
135
+ Defined and enforced in [docs/contracts/](../contracts/README.md):
136
+
137
+ 1. **MomentEnvelope** (Mobile → Engine) — `moment.schema.json`
138
+ 2. **ControlFrame** ack/error/status (Engine → Mobile) — `control.schema.json`
139
+ 3. **NarratedScene** (Engine → Viewer & Library) — `narrated-scene.schema.json`
140
+ 4. **SceneAudio** (Engine → Mobile) — `scene-audio.schema.json`
141
+ 5. **Session context spec** (engine-internal, but the metadata mobile must
142
+ supply is part of MomentEnvelope) — in the contracts README
143
+
144
+ Contract set 1.1.0 incorporates the adversarial review (Codex GPT-5 +
145
+ Gemini 3.1 Pro, both "needs_changes" → all blockers resolved): admission
146
+ acks, error frames, backpressure, freshness deadlines, SSE resume,
147
+ visibility-mutation endpoint, lockstep versioning policy.
148
+
149
+ Enforcement: JSON Schemas are the source of truth; every team's CI validates
150
+ its golden samples against them (`tests/test_contracts.py` pattern); schemas
151
+ are semver'd in their `$id`; breaking changes bump major and require a
152
+ migration note in the PR.
docs/product/rehearsal-2026-06-12.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dress rehearsal — 2026-06-12
2
+
3
+ - Engine: `ws://127.0.0.1:8077` (session `rehearsal-20260612T183149Z`)
4
+ - Video: `/Users/carlos/small-cuts-fixtures/videos/IMG_8175.MOV` — 3 gated moments, style `symmetrist`
5
+ - Narrator: `Qwen/Qwen3-VL-8B-Instruct-GGUF` via `llama_cpp`
6
+ - TTS: `hexgrad/Kokoro-82M`
7
+
8
+ ## Per-moment latency
9
+
10
+ Engine numbers come from the persisted scene's `engine.latency_ms`; wall is the
11
+ client-measured send→SceneAudio round trip (includes WS transfer + queueing).
12
+
13
+ | # | Narration (first 80 chars) | narration_ms | tts_ms | total_ms (engine) | wall_ms (client) | narration ≤4500 | tts ≤4000 |
14
+ |---|---|---|---|---|---|---|---|
15
+ | 1 | The man in the foreground holds a glass tilted at precisely the angle a bartende | 9198 | 7657 | 16855 | 16935 | FAIL | FAIL |
16
+ | 2 | The man’s hand rests just above his temple, fingers brushing the thick black fra | 3701 | 2022 | 5723 | 5792 | PASS | PASS |
17
+ | 3 | The man’s hand hovers over a bottle with a coral cap, its label blurred like a f | 4226 | 2533 | 6759 | 6850 | PASS | PASS |
18
+
19
+ ## Budget comparison (docs/product/architecture.md, Latency budget v1)
20
+
21
+ Warm = moments after the first; moment 1 pays the llama-server spawn and
22
+ Kokoro weight load once per engine process.
23
+
24
+ | Budget line | Target | Measured (warm max) | Verdict |
25
+ |---|---|---|---|
26
+ | Narration, warm llama.cpp | ≤ 4500 ms | 4226 ms | PASS |
27
+ | TTS, warm Kokoro | ≤ 4000 ms | 2533 ms | PASS |
28
+ | End-to-end warm p50 (client wall) | ≤ 10000 ms | 6321 ms | PASS |
29
+
30
+ Cold first moment: narration 9198 ms, tts 7657 ms, wall 16935 ms (one-time per engine process).
31
+ All-moments e2e p50 including the cold first moment: 6850 ms.
32
+ Moments narrated: 3/3; audio: 40.8s, 34.4s, 43.9s.
33
+
34
+ ## Full narrations
35
+
36
+ **Moment 1** (scene `ba7978ea-46e5-4117-a5c6-5f91af79e9b2`):
37
+
38
+ > The man in the foreground holds a glass tilted at precisely the angle a bartender might approve, its rim catching the amber glow of the single, perfect circle of light above. Behind him, the bar’s menus—Cocktails and Shots—hang like miniature, illuminated maps, their text sharp against the dark wood. A neon sign to the left, glowing in a warm, slightly garish red, reads “LOVE, LOVE WILL APART” in a font that looks like it was cut from a child’s coloring book. The scene is framed with the quiet symmetry of a still life: the man’s glasses, the glass, the light, the menus, the sign—all arranged with the careful, slightly off-kilter precision of a Wes Anderson storyboard.
39
+
40
+ **Moment 2** (scene `20f0dfb0-b2c7-4de5-912d-a1da648abdaf`):
41
+
42
+ > The man’s hand rests just above his temple, fingers brushing the thick black frames of his glasses, as if adjusting them for a portrait. Two empty glasses, one with a star etched on its base, sit beside him on the table, catching the warm, amber glow from the bar’s recessed lighting. Behind him, the bar’s shelving is symmetrically arranged with bottles, and a single, glowing sign hangs above the counter like a tiny, silent lantern. The scene is framed with the quiet geometry of a storybook, where every angle is intentional and every shadow holds a secret.
43
+
44
+ **Moment 3** (scene `0f514958-508e-4083-9ba2-58795f036885`):
45
+
46
+ > The man’s hand hovers over a bottle with a coral cap, its label blurred like a forgotten story, while the amber glow of the overhead orb casts a warm, uneven light across his glasses and the rim of his glass. Behind him, the bar’s backlit menu glows with a faint, orderly precision, its rows of text standing like silent, illuminated tombstones in the dim. The scene is framed with the quiet symmetry of a still life—his elbow, the bottle, the glass, all aligned under the single, golden eye of the lamp. There is a small, formal detail: the red of the cap, the yellow of the light, the dark brown of his sweater, all arranged with the careful, melancholic balance of a Wes Anderson storyboard.
47
+
docs/progress.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Progress Tracker
2
+
3
+ | Date | Item | Status | Evidence |
4
+ |---|---|---|---|
5
+ | 2026-06-11 | Hackathon rules verified from primary sources | ✅ | docs/hackathon-rules.md |
6
+ | 2026-06-11 | Product strategy + architecture decided | ✅ | docs/product-strategy.md, docs/architecture.md |
7
+ | 2026-06-11 | Repo bootstrap: CI, lint, tests, secret scan, docs | ✅ | .github/workflows/ci.yml |
8
+ | 2026-06-11 | Vertical slice: Gradio app w/ mock backend + tests | ✅ | `uv run pytest`, `app.py` |
9
+ | 2026-06-11 | KB notes written (mirrored in `kb/`, pending import to canonical KB) | ✅ | kb/*.md |
10
+ | 2026-06-11 | Strategy confirmed by Carlos (+ Live Mode requirement added) | ✅ | docs/product-strategy.md |
11
+ | 2026-06-11 | `main` branch created (from bootstrap commit) | ✅ | `git push origin claude/adoring-clarke-49l3uk:main` |
12
+ | 2026-06-11 | M1 eval harness built + tested | ✅ | `src/small_cuts/eval.py`, `tests/test_eval.py` |
13
+ | 2026-06-11 | Self-hosted runner on Spark registered + verified (GB10, CUDA 13.0, uv OK) | ✅ | Actions run 27370230783 (eval-on-spark on `eval/run-001`) |
14
+ | — | Branch protection on `main` | ⚠️ Carlos reruns the `gh api` command (docs/setup.md) | — |
15
+ | 2026-06-12 | run-004 eval (gemma + Qwen-3B) on Spark, HEIC photos | ✅ | Actions run 27374284507, docs/eval/run-004-scored.md |
16
+ | 2026-06-12 | Automated judge: Codex (GPT-5 vision) scores narrations vs actual photos | ✅ | photo relay workflow + codex exec loop, ~7 min/report |
17
+ | 2026-06-12 | Prompt A/B judged: v2 "find the story" was a pure loss; reverted+hardened as v3, temp 0.7→0.3 | ✅ | docs/eval/prompt-ab-comparison.md |
18
+ | 2026-06-12 | M2 title-card renderer: Claude spec+tests, Codex implementation, 22/22 green | ✅ | docs/specs/title-card.md, src/small_cuts/title_card.py |
19
+ | 2026-06-12 | 5 real glasses videos staged (Spark `~/eval-videos`) + relayed locally | ✅ | fetch-eval-photos workflow |
20
+ | 2026-06-12 | run-005 judged: v3+temp lifted all models (3B pass cells 13%→30%); none meets "most images" bar | ✅ | docs/eval/run-005-scored.md |
21
+ | 2026-06-12 | M1 provisional pick: **Qwen2.5-VL-7B** (best G 3.40 + best V 3.03); DEFAULT_MODEL_ID updated | ✅ | src/small_cuts/narrator.py |
22
+ | — | M1 confirm: run-006 challenger (Qwen3-VL-8B, the KB's original target) executing on Spark | ⏳ | Actions run 27399170174, issue #10 |
23
+ | 2026-06-14 | Review-3 viewer: single playback clock (custom file-backed `<audio>` — gr.Audio plays via wavesurfer, can't be the clock) couples video+captions+progress; theater layout (stage + gallery rail, no scrollbars desktop+mobile); 9:16 preserved; Voice-Cut app icon | ✅ | `src/small_cuts/viewer.py`, KB `2026-06-14-small-cuts-review3-*`, 153 pytest, Playwright 1440+390 |
24
+ | — | M2: TTS + title card + Off-Brand theme | ⬜ | — |
25
+ | — | M3: Space live under hackathon org | ⬜ | — |
26
+ | — | M4: demo video + social post + Field Notes | ⬜ | — |
27
+ | — | M5: submission | ⬜ deadline June 15 | — |
28
+
29
+ ## Decisions Carlos must make / confirm
30
+
31
+ 1. ~~Confirm Track 2 + quest set~~ ✅ confirmed 2026-06-11, with Live Mode added as a requirement-if-feasible.
32
+ 2. ~~Solo vs. team~~ ✅ solo.
33
+ 3. ~~Claim credits~~ ✅ done.
34
+ 4. Enable branch protection — rerun the `gh api` command now that `main` exists (docs/setup.md).
35
+ 5. Verify exact submission deadline time + video/post requirements in Discord (Carlos, in progress).
36
+ 6. ~~Run the M1 eval on the DGX Spark~~ → infrastructure verified working (run 27370230783 reached the GB10). Remaining: stage ~12 personal photos in `/home/carlos/eval-photos` on the Spark, then re-trigger (`gh workflow run eval-on-spark` from a tailnet machine, or push an `eval/**` branch).
docs/regular-pipeline-library-2026-06-15.md ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Regular Pipeline Library - 2026-06-15
2
+
3
+ Working timestamp: 2026-06-15 08:34 UTC / 10:34 CEST.
4
+
5
+ Purpose: replace curated seed narrations with scenes generated through the same `/v1/session`
6
+ WebSocket path used by the iPhone capture app, while preserving an honest but more watchable
7
+ library for the judged Space.
8
+
9
+ ## Runtime Changes
10
+
11
+ - Active library root: `/private/tmp/small-cuts-demo-library-generated-24f-20260615`.
12
+ - The first 24-frame run selected the final frame of each clip. It was technically valid but weaker
13
+ narratively, so its rows were archived into SQLite table
14
+ `hidden_scenes_20260615_final_frame_24f`.
15
+ - The final public run uses key-marked moment envelopes: one selected frame plus 23 preceding
16
+ supplemental frames, matching the user pressing Mark when the moment is readable.
17
+ - Curated hero rows remain archived in
18
+ `/private/tmp/small-cuts-demo-library-hero-20260615.pre-generated-backup-20260615T080924Z`.
19
+ - Final generated session:
20
+ `regular-pipeline-24f-keymark-glasses-20260615T083000Z`.
21
+ - Public Cloudflare read gate lists exactly five generated public scenes.
22
+
23
+ ## Generated Scenes
24
+
25
+ | Seq | Source clip | Scene ID | Wall time | MP4 | Generated title |
26
+ |---:|---|---|---:|---|---|
27
+ | 0 | `desk-laptop.mp4` | `fea81fe7-da00-42e4-b68e-429038a10122` | 4573 ms | 24 frames @ 6 fps | A hand, palm up, blocks the camera's view, revealing a... |
28
+ | 1 | `the-stumble.mp4` | `08318a62-8ae6-41f6-a518-bcc037ac4873` | 4285 ms | 24 frames @ 6 fps | The camera lingers on a concrete alleyway at night, the... |
29
+ | 2 | `street-parked-car.mp4` | `906159c4-5ebd-4e76-9c45-c43e46b85913` | 4656 ms | 24 frames @ 6 fps | The car door is open, revealing a black interior and a... |
30
+ | 3 | `night-drive.mp4` | `df71c033-9f24-4e41-a3e8-3e7c2a31620a` | 4494 ms | 24 frames @ 6 fps | The driver grips the steering wheel with both hands, thumbs... |
31
+ | 4 | `rayuela.mp4` | `aead3e4d-2459-4638-81b8-74abddd2a149` | 4268 ms | 24 frames @ 6 fps | The yellow circle with numbers is painted on the floor... |
32
+
33
+ All five scenes have `frame.jpg`, `card.webp`, `voice.wav`, and `clip.mp4`.
34
+
35
+ ## Generated Narrations
36
+
37
+ **desk-laptop.mp4**
38
+
39
+ > A hand, palm up, blocks the camera's view, revealing a translucent overlay of a retro video game with a small plane and a vertical stack of pixelated blocks. Behind it, a laptop keyboard and a smartphone lie on a desk, the phone screen showing a circular loading icon. The hand remains still, as if posing for a photo, though the game's score reads "S000 60s."
40
+
41
+ **the-stumble.mp4**
42
+
43
+ > The camera lingers on a concrete alleyway at night, the only light spilling from a doorway marked "MANCHESTER" and a glass door to the right. A single, slightly-too-bright bulb hangs overhead, casting long shadows across the wet pavement. The glass door is ajar, revealing a dimly lit interior. A small, yellow warning sign is taped to the pillar beside it.
44
+
45
+ **street-parked-car.mp4**
46
+
47
+ > The car door is open, revealing a black interior and a steering wheel with a logo, while the driver's hand rests on the edge of the door frame. A white SUV is parked directly behind, its rear bumper touching the car's front fender. A man in dark clothing walks past a small kiosk on the sidewalk, glancing toward the parked cars. The sun casts sharp, angular shadows across the pavement and the cars' surfaces.
48
+
49
+ **night-drive.mp4**
50
+
51
+ > The driver grips the steering wheel with both hands, thumbs resting on the top rim, as the car moves forward through a dimly lit parking garage. The dashboard lights glow blue, and the windshield shows streaks of artificial light from distant fixtures, blurred by motion. The steering wheel is black, and the driver's wrists are bare. The car's speedometer needle is steady, indicating a slow, deliberate pace.
52
+
53
+ **rayuela.mp4**
54
+
55
+ > The yellow circle with numbers is painted on the floor behind the metal grid, as if marking a spot for something that never happened. The grid itself is slightly bent, casting a distorted view of the empty court beyond. The blue padding on the pillars is faded, and the numbers on the circle are slightly smudged. The space is quiet, waiting for something that hasn't arrived.
56
+
57
+ ## Quality Read
58
+
59
+ - Strongest current generated cut: `rayuela.mp4`; it is visually legible and has the best generated
60
+ deadpan line.
61
+ - Strong physical POV cut: `street-parked-car.mp4`; the mark point now lands on the open car door.
62
+ - `desk-laptop.mp4` remains literal because the overlay/game UI dominates the selected frame, but it
63
+ is coherent and useful as a builder/desk sample.
64
+ - The key-marked run is materially better than the final-frame run while staying honest: no human
65
+ narration or title copy is injected.
66
+
67
+ ## Verification
68
+
69
+ - Local Python gate: `uv run ruff check . && uv run ruff format --check . && uv run pytest` passed
70
+ with 172 tests.
71
+ - iOS simulator gate: `xcodebuild test ... -scheme SmallCuts` passed with 64 tests, 1 live-engine
72
+ smoke skipped, 0 failures.
73
+ - Public read gate returns five generated scenes.
74
+ - Public `/v1/session` and `PATCH /v1/scenes/*` return `403`.
75
+ - Public media smoke returns `200` for each scene's `clip.mp4`, `voice.wav`, `frame.jpg`, and
76
+ `card.webp`.
77
+ - HF Space Gradio `_tick` returns the latest generated title, video stage, Cloudflare audio URL,
78
+ visibility `public`, and five gallery captions.
docs/setup.md ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Setup Guide
2
+
3
+ ## Local development
4
+
5
+ ```bash
6
+ git clone https://github.com/macayaven/small-cuts.git
7
+ cd small-cuts
8
+ uv sync --extra dev # or: pip install -e ".[dev]"
9
+
10
+ # fastest loop — deterministic mock backend, no model downloads
11
+ SMALL_CUTS_BACKEND=mock uv run python app.py
12
+ ```
13
+
14
+ Real-model backends (M1+):
15
+
16
+ ```bash
17
+ uv sync --extra local # transformers + torch
18
+ SMALL_CUTS_BACKEND=transformers SMALL_CUTS_MODEL_ID=HuggingFaceTB/SmolVLM2-2.2B-Instruct \
19
+ uv run python app.py
20
+
21
+ uv sync --extra llama # llama-cpp-python
22
+ SMALL_CUTS_BACKEND=llama_cpp SMALL_CUTS_GGUF_PATH=/path/to/model.gguf \
23
+ uv run python app.py
24
+ ```
25
+
26
+ ## Environment variables
27
+
28
+ | Variable | Default | Meaning |
29
+ |---|---|---|
30
+ | `SMALL_CUTS_BACKEND` | `mock` | `mock` \| `transformers` \| `llama_cpp` |
31
+ | `SMALL_CUTS_MODEL_ID` | `HuggingFaceTB/SmolVLM2-2.2B-Instruct` | HF model id for the transformers backend |
32
+ | `SMALL_CUTS_GGUF_PATH` | — | local GGUF path for the llama.cpp backend |
33
+
34
+ ## Secrets
35
+
36
+ There are **no secrets in this repo** and the core app needs none at runtime.
37
+
38
+ - Local dev: fetch tokens (HF write token for Space deploys) from **1Password
39
+ Connect** — never paste them into files or shell history that gets committed.
40
+ - CI: `GITHUB_TOKEN` only (provided by Actions).
41
+ - Space: set `HF_TOKEN`-class secrets in the Space settings UI if ever needed.
42
+ - `gitleaks` runs in CI on every push and PR.
43
+
44
+ ## Branch protection (one-time admin step — ⚠️ pending)
45
+
46
+ GitHub MCP tooling in the bootstrap session could not set branch protection.
47
+ Carlos (or any session with `gh` + admin) must run once:
48
+
49
+ ```bash
50
+ gh api -X PUT repos/macayaven/small-cuts/branches/main/protection \
51
+ -F required_status_checks[strict]=true \
52
+ -F 'required_status_checks[contexts][]=ci' \
53
+ -F enforce_admins=true \
54
+ -F required_pull_request_reviews[required_approving_review_count]=0 \
55
+ -F restrictions=null
56
+ ```
57
+
58
+ (Zero required approvals keeps the PR-based workflow without blocking a
59
+ solo builder on a 4-day deadline.)
60
+
61
+ ## Tailnet access from cloud Claude Code sessions
62
+
63
+ Cloud session containers are not on the tailnet by default. `scripts/tailnet-connect.sh`
64
+ fixes that: it installs Tailscale (userspace networking — no TUN in these
65
+ containers), authenticates with `TS_AUTHKEY`, and writes an SSH config so
66
+ `ssh spark` / `ssh mac-studio` dial through `tailscale nc`.
67
+
68
+ One-time setup by Carlos:
69
+
70
+ 1. Tailscale admin console → **Settings → Keys → Generate auth key** with
71
+ **Reusable + Ephemeral + Pre-approved** (ephemeral nodes vanish when the
72
+ container dies), ideally tagged (e.g. `tag:claude-session`) with ACLs
73
+ limiting it to SSH toward `spark-caeb` and `mac-studio`.
74
+ 2. Add it as `TS_AUTHKEY` in the Claude Code **environment settings**
75
+ (code.claude.com → environment → secrets/env vars) — never in the repo.
76
+ 3. Recommended: enable **Tailscale SSH** on both machines
77
+ (`sudo tailscale set --ssh`) with a matching ACL `ssh` rule, so sessions
78
+ need no SSH keypair management at all.
79
+ 4. Optional, to auto-connect every session: add a `SessionStart` hook to
80
+ `.claude/settings.json` running `bash scripts/tailnet-connect.sh || true`.
81
+
82
+ Each session then runs (or the hook runs): `bash scripts/tailnet-connect.sh`.
83
+
84
+ ## Deploying the Space (M3)
85
+
86
+ 1. Create the Space **under the hackathon org**: `build-small-hackathon/small-cuts`
87
+ (Gradio SDK, ZeroGPU hardware if available, CPU fallback otherwise).
88
+ 2. Push this repo's `app.py`, `src/`, `requirements.txt` to the Space
89
+ (`gradio deploy` or a `git push` to the Space remote).
90
+ 3. Set `SMALL_CUTS_BACKEND=transformers` (ZeroGPU) or `llama_cpp` (CPU) in
91
+ Space variables.
92
+ 4. Smoke-test from a phone browser — that is the judge experience.
docs/specs/llama-backend.md ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Spec: LlamaCppBackend via llama-server (M3, issue #14 → achievement:llama)
2
+
3
+ ## Purpose
4
+
5
+ Make `SMALL_CUTS_BACKEND=llama_cpp` real: the narrator runs through the
6
+ llama.cpp runtime (Llama Champion badge). Verified end-to-end on 2026-06-12
7
+ (see #14 comment): official `Qwen/Qwen3-VL-8B-Instruct-GGUF` Q4_K_M + mmproj
8
+ through llama.cpp b9590 produces grounded, on-voice narrations.
9
+
10
+ Design: a managed **`llama-server` subprocess** spoken to over its
11
+ OpenAI-compatible HTTP API — NOT llama-cpp-python chat handlers (the Python
12
+ wrapper lags upstream on new vision archs; the server tracks llama.cpp).
13
+
14
+ ## Contract (`src/small_cuts/narrator.py`, replace the stub)
15
+
16
+ `LlamaCppBackend` keeps `name = "llama_cpp"` and the `Backend` protocol.
17
+
18
+ ### Configuration (env)
19
+
20
+ - `SMALL_CUTS_LLAMA_URL` — if set (e.g. `http://127.0.0.1:8080`), use this
21
+ already-running llama-server and manage NO subprocess. This is the CI/test
22
+ seam and the local-dev path (`brew install llama.cpp; llama-server …`).
23
+ - `SMALL_CUTS_LLAMA_SERVER` — path to a `llama-server` binary; default:
24
+ `shutil.which("llama-server")`. If neither URL nor binary is available,
25
+ `generate` raises `RuntimeError` with an actionable message (mention brew
26
+ and SMALL_CUTS_LLAMA_URL). Do NOT auto-download binaries in this package.
27
+ - `SMALL_CUTS_GGUF_PATH` / `SMALL_CUTS_MMPROJ_PATH` — local GGUF paths; if
28
+ unset, resolve via `huggingface_hub.hf_hub_download` from
29
+ `Qwen/Qwen3-VL-8B-Instruct-GGUF` (`Qwen3VL-8B-Instruct-Q4_K_M.gguf`,
30
+ `mmproj-Qwen3VL-8B-Instruct-F16.gguf`). `model_id` reports the repo id or
31
+ the basename of the local GGUF.
32
+
33
+ ### Subprocess management (only when no SMALL_CUTS_LLAMA_URL)
34
+
35
+ - Lazily started on first `generate` (constructing the backend must not
36
+ spawn anything — mirrors the lazy `_load` pattern).
37
+ - Launch: `llama-server -m <gguf> --mmproj <mmproj> --port <free port>
38
+ -c 8192 --image-max-tokens 1024 --host 127.0.0.1`.
39
+ The image-token floor matters: llama.cpp warns Qwen-VL needs ≥ 1024 image
40
+ tokens for grounding accuracy. Keep exactly 1024.
41
+ - Wait for readiness by polling `GET /health` (llama-server exposes it)
42
+ with a deadline (~120 s — model load takes a while); kill + RuntimeError
43
+ on timeout. Register `atexit` cleanup that terminates the child.
44
+
45
+ ### Request path
46
+
47
+ - Reuse `build_messages(style_key, scene_hint)`; system message as-is; user
48
+ content = `[{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,…"}},
49
+ {"type": "text", "text": <user text>}]` (OpenAI vision shape llama-server accepts).
50
+ - **Downscale before encoding**: new pure helper in narrator.py
51
+ `def _downscale(image: Image.Image, max_side: int = 1024) -> Image.Image`
52
+ — proportional thumbnail when the longest side exceeds `max_side`
53
+ (full-res portrait glasses frames blow up vision compute; see #14).
54
+ JPEG quality 90.
55
+ - Body: `temperature` from `SMALL_CUTS_TEMPERATURE` (default 0.3),
56
+ `max_tokens: 160`. Parse `choices[0].message.content`, return `.strip()`.
57
+ - Use `httpx` (already a Gradio dependency) with a generous timeout (120 s).
58
+ - Connection/HTTP errors → `RuntimeError` mentioning the server URL.
59
+
60
+ ## Out of scope
61
+
62
+ - NO ui.py changes (P1 rewrites the UI; backend selection stays env-based).
63
+ - NO binary auto-download; NO Space wiring (separate deploy step).
64
+
65
+ ## Verification
66
+
67
+ `tests/test_llama_backend.py` (committed alongside, uses a fake in-process
68
+ HTTP server — no llama binary in CI) must pass, plus the full gate:
69
+ `uv run pytest && uv run ruff check && uv run ruff format --check`.
70
+ Do not modify tests or spec; if a test looks wrong, stop and explain.
71
+ Manual (not CI): with a local `llama-server` running,
72
+ `SMALL_CUTS_LLAMA_URL=http://127.0.0.1:8080 SMALL_CUTS_BACKEND=llama_cpp uv run python -c "…narrate(Image.open(...))…"`.
docs/specs/theme.md ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Spec: Off-Brand custom Gradio theme (M2, issue #13)
2
+
3
+ ## Purpose
4
+
5
+ Replace the placeholder `gr.themes.Monochrome` in `ui.py` with a custom
6
+ cinematic theme — the **Off-Brand** quest (own visual identity). The house
7
+ style already exists in `title_card.STYLE_CARDS`; the theme borrows from it:
8
+ a dark screening-room canvas with gold marquee accents.
9
+
10
+ ## Contract
11
+
12
+ ### 1. `src/small_cuts/theme.py` (new module)
13
+
14
+ ```python
15
+ class OffBrand(gr.themes.Base): ...
16
+
17
+ def build_theme() -> gr.themes.Base
18
+ ```
19
+
20
+ - `OffBrand.__init__` configures fonts: body/headings `Spectral`
21
+ (GoogleFont, serif fallback) — the existing house font — and
22
+ `IBM Plex Mono` (GoogleFont, monospace fallback) for code/metadata.
23
+ - Then `.set()` overrides establishing the palette (light AND `*_dark`
24
+ variants set to the same values — the screening room ignores OS mode):
25
+ - `body_background_fill` / `body_background_fill_dark`: `#101014`
26
+ (charcoal, trailer card background)
27
+ - `body_text_color` / `body_text_color_dark`: `#E8E4D8` (bone white,
28
+ noir card text)
29
+ - `button_primary_background_fill` / `_dark`: `#D4AF37` (marquee gold)
30
+ - `button_primary_text_color` / `_dark`: `#101014`
31
+ - `block_background_fill` / `_dark`: a slightly lifted charcoal
32
+ (e.g. `#16161C`)
33
+ - `block_title_text_color` / `_dark`: `#D4AF37`
34
+ - plus whatever minimal extra overrides (borders, input fills, link
35
+ color) are needed so no component renders as a glaring white patch on
36
+ the dark canvas. Use STYLE_CARDS hexes where a choice is needed.
37
+ - `build_theme()` returns a constructed `OffBrand()` instance.
38
+ - Module stays small (≈60 lines); no new dependencies.
39
+
40
+ ### 2. `src/small_cuts/ui.py`
41
+
42
+ Replace the placeholder assignment (and its comment):
43
+
44
+ ```python
45
+ THEME = gr.themes.Monochrome(font=[gr.themes.GoogleFont("Spectral"), "serif"])
46
+ ```
47
+
48
+ with an import from the new module:
49
+
50
+ ```python
51
+ from .theme import build_theme
52
+ THEME = build_theme()
53
+ ```
54
+
55
+ `app.py` already does `demo.launch(theme=THEME)` (Gradio 6 API) — do not
56
+ change it, and do not touch anything else in `ui.py` (parallel changes wire
57
+ title cards and TTS into the same file; keep this diff to the THEME swap).
58
+
59
+ ## Verification
60
+
61
+ `tests/test_theme.py` (committed alongside this spec) must pass, plus the
62
+ full local gate: `uv run pytest && uv run ruff check && uv run ruff format --check`.
63
+ Do not modify the tests; if a test looks wrong, flag it instead.
docs/specs/title-card-wiring.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Spec: Title-card wiring + title derivation (M2, issue #12)
2
+
3
+ ## Purpose
4
+
5
+ `title_card.render_title_card` exists but nothing calls it. Wire it into the
6
+ Gradio app so every narration is accompanied by its movie-style title card,
7
+ with the title derived from the narration's first clause (caller-side, per
8
+ docs/specs/title-card.md "out of scope" note).
9
+
10
+ ## Contract
11
+
12
+ ### 1. `derive_title` (new, in `src/small_cuts/title_card.py`)
13
+
14
+ ```python
15
+ def derive_title(text: str, max_len: int = 60) -> str
16
+ ```
17
+
18
+ - Strips surrounding whitespace.
19
+ - Strips one leading bracketed style tag if present (`"[Noir Detective] The
20
+ rain…"` → `"The rain…"`) — the mock backend emits narrations in this shape.
21
+ - Returns the **first clause**: everything before the first sentence-ending
22
+ punctuation (`.`, `!`, `?`, `;`) or em-dash (`—`). The terminator itself is
23
+ not included. Trailing ellipsis in the source ("Lunch... changes") must not
24
+ produce an empty or one-word title: a `...`/`…` only terminates the clause
25
+ when followed by whitespace + an uppercase letter, otherwise it is kept.
26
+ (Simplification allowed: treat `...` like any terminator BUT only after the
27
+ clause already has ≥ 3 words.)
28
+ - If the clause is longer than `max_len` characters, cut at the last word
29
+ boundary that fits and append `…` (single ellipsis char). Never cut
30
+ mid-word; never return more than `max_len` chars total.
31
+ - Empty / whitespace-only input → `"Untitled Scene"`.
32
+ - Pure + deterministic. No new dependencies.
33
+
34
+ ### 2. UI wiring (`src/small_cuts/ui.py`)
35
+
36
+ - Right column gains a card display **above** the narration textbox:
37
+ `gr.Image(label="Title card", interactive=False)`.
38
+ - `_narrate_handler` and `_narrate_video_handler` now return
39
+ `tuple[PIL.Image.Image, str]` = `(card, narration_text)`:
40
+ `card = render_title_card(derive_title(text), style_key)` at the default
41
+ 1280×720 size, using the **requested** style key.
42
+ - The no-image / no-video placeholder paths also render a card from the
43
+ placeholder text (the show must go on).
44
+ - All three event wirings (`go.click`, `image.change`, `video.change`) get
45
+ `outputs=[card, narration]`.
46
+
47
+ ## Verification
48
+
49
+ `tests/test_title_wiring.py` (committed alongside this spec) must pass, plus
50
+ the full local gate: `uv run pytest && uv run ruff check && uv run ruff format --check`.
51
+ Do not modify the tests; if a test looks wrong, flag it instead.
docs/specs/title-card.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Spec: Title-card renderer (M2)
2
+
3
+ ## Purpose
4
+
5
+ Every narration gets a movie-style title card: a 16:9 image shown beside the
6
+ narration in the Gradio app, styled per director. Part of the **Off-Brand**
7
+ quest (custom visual identity) — pure PIL, no model, no network.
8
+
9
+ ## Contract
10
+
11
+ New module `src/small_cuts/title_card.py`:
12
+
13
+ ```python
14
+ def render_title_card(
15
+ title: str,
16
+ style_key: str,
17
+ size: tuple[int, int] = (1280, 720),
18
+ ) -> PIL.Image.Image
19
+ ```
20
+
21
+ - Returns an RGB image of exactly `size`.
22
+ - `style_key` must be a key of `styles.STYLES`; unknown keys raise `KeyError`
23
+ (same behavior as `build_messages`).
24
+ - **Deterministic**: identical inputs produce byte-identical output. No
25
+ randomness, no timestamps.
26
+ - Layout: a kicker line (`A SMALL CUTS PICTURE`), the title (uppercased,
27
+ wrapped, centered, dominant), and a style label subtitle (from
28
+ `STYLES[style_key].label`).
29
+ - Title text wraps to fit; very long titles (200+ chars) must render without
30
+ raising or overflowing the canvas.
31
+ - Empty title renders the kicker + subtitle only (no crash).
32
+
33
+ ## Per-style art direction
34
+
35
+ One palette + typographic treatment per style, keyed off `STYLES`:
36
+
37
+ | style | background | text | treatment |
38
+ |---|---|---|---|
39
+ | deadpan | warm off-white `#F2EFE6` | near-black `#1A1A1A` | thin rule above+below title |
40
+ | noir | black `#0D0D0F` | bone white `#E8E4D8` | hard white 2px frame inset |
41
+ | nature_doc | deep green `#0E2A1B` | cream `#F0E9D2` | double-line frame |
42
+ | trailer | charcoal `#101014` | gold `#D4AF37` | wide letter-spacing (inserted spaces) |
43
+ | telenovela | crimson `#5C0A14` | rose white `#FFE9EC` | ornament line `♦ ─── ♦` under title |
44
+ | symmetrist | pastel pink `#F7D6C9` | brown `#5B3A29` | perfectly centered, thin border 8px inset |
45
+
46
+ Every key in `STYLES` must have an entry — add a module-level mapping and a
47
+ test guards the invariant (new styles must add palettes).
48
+
49
+ ## Constraints
50
+
51
+ - Fonts: `ImageFont.load_default(size=...)` (Pillow ≥ 10.1 scalable default)
52
+ — no bundled font files, no new dependencies.
53
+ - Must work headless (no display) — CI runs it.
54
+ - Keep the module under ~120 lines; this is a hackathon, not a layout engine.
55
+
56
+ ## Out of scope (M2 later steps)
57
+
58
+ - Deriving the title from the narration text (caller's job).
59
+ - Animated cards, gradients, photo-blending.
60
+ - Gradio wiring (separate change).
docs/specs/tts.md ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Spec: Kokoro TTS behind the backend pattern (M2, issue #11)
2
+
3
+ ## Purpose
4
+
5
+ Give the narrator a voice. New module `src/small_cuts/tts.py` mirroring the
6
+ `narrator.py` backend pattern exactly (Protocol + mock + real, env-var
7
+ selection), plus an on-demand "read it to me" button in the Gradio app.
8
+ All local — no cloud TTS (Off the Grid quest).
9
+
10
+ ## Contract
11
+
12
+ ### 1. `src/small_cuts/tts.py`
13
+
14
+ ```python
15
+ @dataclass(frozen=True)
16
+ class Speech:
17
+ sample_rate: int
18
+ audio: np.ndarray # mono float32, values in [-1.0, 1.0]
19
+ backend: str
20
+ model_id: str
21
+ latency_s: float
22
+
23
+ class TTSBackend(Protocol):
24
+ name: str
25
+ model_id: str
26
+ def synthesize(self, text: str) -> tuple[int, np.ndarray]: ...
27
+
28
+ def get_tts_backend(name: str | None = None) -> TTSBackend
29
+ def speak(text: str, backend: TTSBackend | None = None) -> Speech
30
+ ```
31
+
32
+ - `get_tts_backend`: reads `SMALL_CUTS_TTS_BACKEND` env var, default `mock`;
33
+ unknown name → `ValueError("Unknown TTS backend …")` (mirror
34
+ `narrator.get_backend`).
35
+ - `speak`: empty / whitespace-only text → `ValueError` mentioning "empty";
36
+ otherwise times the backend call and returns a `Speech`.
37
+
38
+ ### 2. `MockTTSBackend` — `name = "mock"`, `model_id = "mock-tts-0"`
39
+
40
+ Deterministic, weightless, CI-safe:
41
+ - 24_000 Hz mono float32 sine wave, amplitude ≤ 0.3.
42
+ - Duration scales with text length, clamped to [0.2s, 2.0s].
43
+ - Pitch derives from the text content (e.g. `220 + sum(text.encode()) % 220`
44
+ Hz) so different texts produce different audio, same text identical audio.
45
+
46
+ ### 3. `KokoroBackend` — `name = "kokoro"`, `model_id = "hexgrad/Kokoro-82M"`
47
+
48
+ - Lazy: constructing the backend must NOT import `kokoro`; first
49
+ `synthesize` call does. If the import fails, raise `RuntimeError` with an
50
+ actionable message (mention `uv sync --extra tts`).
51
+ - Uses `kokoro.KPipeline(lang_code="a")`, voice from `SMALL_CUTS_TTS_VOICE`
52
+ env var (default `af_heart`), 24_000 Hz output; concatenate segment audio
53
+ into one mono float32 array.
54
+ - Add optional dependency group to `pyproject.toml`:
55
+ `tts = ["kokoro>=0.9", "soundfile>=0.12"]`. CI does NOT install it; no
56
+ import of kokoro at module import time (CI must stay green without it).
57
+
58
+ ### 4. UI wiring (`src/small_cuts/ui.py`)
59
+
60
+ - Right column, under the narration textbox: a secondary button
61
+ `🔊 Read it to me` and `gr.Audio(label="The narrator speaks…", interactive=False)`.
62
+ - New handler:
63
+ ```python
64
+ def _speak_handler(text: str) -> tuple[int, np.ndarray] | None
65
+ ```
66
+ Returns `None` when `text` is empty/whitespace (clears the player),
67
+ otherwise `(speech.sample_rate, speech.audio)` via `speak(text)`.
68
+ - Wiring: `speak_btn.click(_speak_handler, inputs=[narration], outputs=[audio])`.
69
+ - TTS is on-demand only — narration handlers stay untouched (a parallel
70
+ change wires title cards into them; do not modify their signatures).
71
+
72
+ ## Verification
73
+
74
+ `tests/test_tts.py` (committed alongside this spec) must pass, plus the full
75
+ local gate: `uv run pytest && uv run ruff check && uv run ruff format --check`.
76
+ Do not modify the tests; if a test looks wrong, flag it instead.
77
+ Manual sanity (optional, not CI): `uv sync --extra tts` then
78
+ `SMALL_CUTS_TTS_BACKEND=kokoro uv run python -c "from small_cuts.tts import speak; print(speak('One man. One sandwich.').audio.shape)"`.
docs/specs/video-input.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Spec: video input + device-free Live Mode tests (M3.5 prep)
2
+
3
+ Image flow stays primary; video is additive. Ground truth measured from the
4
+ five real glasses clips (relayed from the Spark, staged at
5
+ `~/small-cuts-fixtures/videos/`, never committed):
6
+
7
+ | Clip | Codec | Resolution | fps | Duration |
8
+ |---|---|---|---|---|
9
+ | IMG_8165.MOV | HEVC | 1424×1904 | 29.9 | 180.0s |
10
+ | IMG_8175.MOV | HEVC | 1376×1824 | 29.7 | 180.0s |
11
+ | IMG_8216.MOV | HEVC | 1552×2064 | 30.0 | 180.0s |
12
+ | IMG_8292.MP4 | H.264 | 976×1296 | 30.0 | 46.9s |
13
+ | IMG_8368.MOV | HEVC | 1504×2016 | 30.0 | 180.4s |
14
+
15
+ Measured facts that override earlier guesses: frames are **natively portrait
16
+ with no rotation side-data** — PyAV decodes them upright, no display-matrix
17
+ handling needed (verified visually on IMG_8292 mid-frame). Real clips run to
18
+ **180s** (glasses max) — the upload cap must be ≥180s, not 60s.
19
+
20
+ ## 1. Video ingest contract (minimal change, three edits)
21
+
22
+ 1. **Extract sampler** → new `src/small_cuts/frames.py`:
23
+ - `sample_frames(path, every_n_seconds=3.0, max_frames=None) -> list[PIL.Image]`
24
+ - Body lifted from `eval.py:_sample_video_frames` minus the sibling-JPEG
25
+ writes — return in-memory RGB PIL Images only.
26
+ - `pick_frame(frames) -> PIL.Image` — middle frame for v1.
27
+ - `eval.py:_sample_video_frames` becomes a thin wrapper that saves the
28
+ returned images (eval behavior unchanged).
29
+ 2. **UI** (`ui.py`): add `gr.Video(sources=["upload"])` beside the existing
30
+ image input; handler = `sample_frames(path)` → `pick_frame` → unchanged
31
+ `narrate()`. No changes to `narrator.py` or the Backend protocol.
32
+ 3. **Deps**: `av>=12.0` moves to core `[project.dependencies]` +
33
+ `requirements.txt` (the Space has no `av` today → instant ImportError).
34
+
35
+ ## 2. Frame→narration strategy
36
+
37
+ - **v1 (ship)**: one narration per clip, middle sampled frame. Eval evidence
38
+ says groundedness is the weak axis on single frames already; multi-image
39
+ prompts add vision tokens and hallucination surface with no evidence yet.
40
+ - **v1.5 (if time)**: sharpest-of-sampled via Laplacian variance, still one
41
+ frame.
42
+ - **Deferred**: multi-frame prompts — gate behind the judged eval. A full
43
+ video eval naively costs 5 clips × ~60 frames × 3 styles × N models ≈
44
+ thousands of generations; `pick_frame` must land first.
45
+
46
+ ## 3. Device-free preliminary tests (real footage, no hardware)
47
+
48
+ - `tests/test_frames.py` (CI-safe, synthetic clips encoded with PyAV into
49
+ tmp_path): frame count at cadence, short-clip ≥1 frame, no side-effect
50
+ files, RGB PIL output, eval wrapper still writes JPEGs.
51
+ - Local-only smoke (skipif fixtures missing): parametrize over the five real
52
+ clips — decodes, ≥1 frame, portrait orientation preserved.
53
+ - One manual Gradio smoke: upload IMG_8292.MP4 → narration appears, frame
54
+ upright, round-trip acceptable. Repeat once on the deployed Space (only
55
+ test that catches missing-`av`-class failures).
56
+
57
+ ## 4. Deferred to real-device work (post-hackathon)
58
+
59
+ Live DAT capture (streaming off the glasses) is out of scope for v1 — clips
60
+ enter via phone export → upload. When the native bridge starts, build against
61
+ MockDeviceKit from day one (`pairRaybanMeta()`, `camera.setCameraFeed(fileURL:)`
62
+ with HEVC fixtures) per the KB note
63
+ `2026-06-11-directors-cut-claude-ios-dat-bridge-reference`. Genuinely needs
64
+ hardware: Meta AI registration, Developer-Mode-after-firmware gotcha, real BT
65
+ bandwidth degradation, tap/hinge quirks.
66
+
67
+ ## 5. Scene-change gate (M3.5, after video input)
68
+
69
+ 32×32 grayscale mean-abs-diff in `frames.py`, threshold tuned by replaying
70
+ the five clips at 1s cadence. Proves frames → gate → narrate on recorded
71
+ footage before any Gradio streaming work.
72
+
73
+ ## Open questions for Carlos
74
+
75
+ 1. Is `gr.Video` upload part of the judged demo, or insurance? If judged,
76
+ the deployed-Space smoke moves up.
77
+ 2. OK to pin Gradio to 6.x now so M3.5 streaming lands on a stable API?
kb/2026-06-11-directors-cut-peer-synthesis-for-small-cuts.md ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ projectId: directors-cut
3
+ id: 2026-06-11-directors-cut-peer-synthesis-for-small-cuts
4
+ status: PARTIAL — written from a cloud session without access to the canonical KB
5
+ ---
6
+
7
+ # Director's Cut — Onboarding Synthesis for Small Cuts (2026-06-11)
8
+
9
+ ## ⚠️ Evidence limitation (read first)
10
+
11
+ This synthesis was produced in a **cloud Claude Code session**, not on the Mac
12
+ Studio. The following prescribed sources were **unreachable** and are NOT
13
+ reflected below:
14
+
15
+ - `.knowledge/INDEX.md`, `00-guidelines/agent-operating-contract.md`,
16
+ `30-resources/cli-app-setup.md`, `10-projects/directors-cut/*` (no `.knowledge/`
17
+ dir, no `knowledge_base-*` MCP tools in this environment)
18
+ - Peer notes from Codex and Claude (live only in the KB above)
19
+ - Source code of `macayaven/directors-cut` and `macayaven/wearables-gradio-poc`
20
+ (GitHub access in this session is scoped to `macayaven/small-cuts` only)
21
+
22
+ Everything below comes from: GitHub repo metadata (names/descriptions/dates),
23
+ Carlos's own emails (Gmail), the Notion workspace (SparkClaw lineage), and the
24
+ official hackathon sources. Items that could not be verified are labeled
25
+ **unverified**. **A Mac Studio session must re-run the prescribed KB reads and
26
+ amend this note.**
27
+
28
+ ## 1. What Director's Cut is
29
+
30
+ A cinematic POV narration product for Meta Ray-Ban (Display) glasses: an
31
+ intelligent narrator that watches what you see and produces film-style
32
+ commentary. Evidence: repo `macayaven/directors-cut` (created 2026-05-18) —
33
+ "iOS companion for Meta Ray-Ban Display glasses — Director's Cut cinematic POV
34
+ narration (Gemini Live cloud / Qwen3-Omni local)".
35
+
36
+ ## 2. Intended emotional/product experience
37
+
38
+ The omniscient narrator characters of *The Invention of Lying*: lived moments
39
+ transformed into meaningful, contextual, cinematic commentary (per Carlos's
40
+ mission statement for this project).
41
+
42
+ ## 3. Hackathon/submission context
43
+
44
+ **Confirmed:** Build Small Hackathon (Gradio × HF), submissions close June 15,
45
+ 2026; Gradio Space under org; ≤32B params; Carlos registered (credits email
46
+ 2026-06-04). **Unverified:** which sidequests Carlos previously "selected";
47
+ whether any submission draft exists under the org (none found by name on
48
+ 2026-06-11); team status with Topi (invited by email 2026-06-03, outcome unknown).
49
+
50
+ ## 4. What Codex contributed
51
+
52
+ **Unknown from this environment** — Codex's notes live in the canonical KB.
53
+ (Repo commit history unreadable here.)
54
+
55
+ ## 5. What Claude contributed
56
+
57
+ **Unknown from this environment** — same limitation. Notion shows extensive
58
+ Claude-authored work on the *predecessor* wearables project (SparkClaw, April
59
+ 2026): bridge architecture, reply-envelope design, demo-thesis notes.
60
+
61
+ ## 6–7. Peer agreement / conflicts
62
+
63
+ **Cannot be assessed without the peer notes.** Known objective tension in the
64
+ trajectory itself: a native iOS + cloud Gemini Live path vs. hackathon rules
65
+ that require a Gradio Space and reward local ≤32B inference. The existence of
66
+ `wearables-gradio-poc` (created 2026-06-07, two days after the hack window
67
+ opened) suggests the pivot was already understood.
68
+
69
+ ## 8. Current architecture (best available evidence)
70
+
71
+ - `directors-cut`: Swift iOS companion; narration via Gemini Live (cloud) or
72
+ Qwen3-Omni (local). Details unverified.
73
+ - `wearables-gradio-poc`: Gradio proof-of-concept, presumed hackathon pivot.
74
+ Contents unverified.
75
+ - Prior art (SparkClaw, Notion-verified): Mac Studio/DGX local-first stack,
76
+ WhatsApp bridge to glasses, vLLM vision on DGX `:8000/v1`, Meta Wearables
77
+ Device Access Toolkit (github.com/facebook/meta-wearables-dat-ios).
78
+
79
+ ## 9. Current implementation state
80
+
81
+ Unverified beyond repo metadata: directors-cut last pushed ~2026-05-21 (2 open
82
+ issues); wearables-gradio-poc created 2026-06-07, no language stats visible.
83
+
84
+ ## 10. Highest-risk assumptions (for any successor)
85
+
86
+ 1. That glasses integration can be load-bearing in a judged demo (it can't —
87
+ judges run the Space).
88
+ 2. That cloud narration (Gemini Live) is acceptable — it forfeits Off the Grid
89
+ and weakens the small-model story.
90
+ 3. That a small VLM can narrate with grounded specificity (needs eval, M1).
91
+ 4. That the remaining 4 days suffice for any native-app work (they don't).
92
+
93
+ ## 11. First 5 actions before coding (executed in this session)
94
+
95
+ 1. ✅ Verify hackathon rules from primary sources (emails + org README).
96
+ 2. ✅ Inventory actually-reachable knowledge (Notion, Gmail, repo metadata).
97
+ 3. ✅ Decide track (2) and quest portfolio.
98
+ 4. ✅ Decide architecture: Space-is-the-product, glasses-as-capture.
99
+ 5. ✅ Bootstrap `small-cuts` repo with discipline + vertical slice.
100
+
101
+ ## 12. Evidence ledger
102
+
103
+ - Gmail thread `19e8db3f17a5a437` (2026-06-03, rules + Carlos's forward to Topi)
104
+ - Gmail thread `19e9480d1438494e` (2026-06-04, registration confirmed, credits, deadline)
105
+ - https://huggingface.co/spaces/build-small-hackathon/README (raw fetch 2026-06-11)
106
+ - https://huggingface.co/build-small-hackathon (org stats, no macayaven space found)
107
+ - GitHub repo metadata via API search `user:macayaven` (2026-06-11)
108
+ - Notion: "SparkClaw Documentation Hub - 2026-04-18" and children (prior project)
kb/2026-06-11-small-cuts-architecture-decision.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ projectId: small-cuts
3
+ id: 2026-06-11-small-cuts-architecture-decision
4
+ ---
5
+
6
+ # Small Cuts — Architecture Decisions (2026-06-11, post-confirmation)
7
+
8
+ - **What changed:** Carlos confirmed Track 2 + quest portfolio and added a
9
+ requirement-if-feasible: real-time narration playback **on the glasses**.
10
+ Decisions taken: (a) Live Mode layered design — Bluetooth audio-out through
11
+ the glasses is free once TTS lands (M2); continuous camera-streaming Live
12
+ Mode is stretch M3.5; glasses-camera capture via Meta Wearables DAT is
13
+ post-hackathon. (b) **DGX Spark** is the M1 eval host (CUDA matches ZeroGPU;
14
+ 128GB fits all candidate VLMs). (c) `main` created from the bootstrap commit
15
+ so branch protection can be applied; PR-based workflow from here on.
16
+ - **Why:** preserves the "hearing your life narrated" magic without making
17
+ hardware load-bearing for judges; keeps the 4-day plan intact.
18
+ - **Evidence / commands:** `git push origin claude/adoring-clarke-49l3uk:main`
19
+ (new branch `main` @ 7ca6d57); eval harness `src/small_cuts/eval.py` +
20
+ `tests/test_eval.py` (pytest green); tailnet unreachable from cloud sessions
21
+ (no MagicDNS resolution for `*.tail48bab7.ts.net`) — eval must run locally.
22
+ - **Current status:** solo entry confirmed; credits claimed; Carlos verifying
23
+ deadline time + submission mechanics in Discord.
24
+ - **Next action:** Carlos runs the eval one-liner on the Spark
25
+ (docs/implementation-plan.md M1) and reruns the branch-protection command.
26
+ - **Risks:** continuous Live Mode could eat M2–M4 time — strictly gated behind
27
+ a green M3; scene-change detection quality unknown until prototyped.
kb/2026-06-11-small-cuts-hackathon-grounding.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ projectId: small-cuts
3
+ id: 2026-06-11-small-cuts-hackathon-grounding
4
+ ---
5
+
6
+ # Small Cuts — Hackathon Grounding (2026-06-11)
7
+
8
+ Canonical detail lives in `docs/hackathon-rules.md` (same repo). Summary:
9
+
10
+ - **Official name:** Build Small Hackathon — "Small Models, Big Adventures" (Gradio · Hugging Face)
11
+ - **Official URL:** https://huggingface.co/build-small-hackathon
12
+ - **Deadline:** submissions close **June 15, 2026** (time-of-day unverified)
13
+ - **Submission format:** Gradio app as a **Space under the hackathon org** + demo video + social post
14
+ - **Selected sidequests (decided 2026-06-11, pending Carlos confirmation):**
15
+ Off the Grid · Off-Brand · Llama Champion · Field Notes · (stretch: Well-Tuned)
16
+ - **Judging criteria:** Track 2 — delight ("show a friend"), AI load-bearing,
17
+ originality, Gradio polish (verbatim in docs/hackathon-rules.md)
18
+ - **Hard constraints:** total model params ≤ 32B; must be a Gradio Space
19
+ - **Allowed/forbidden tools:** no explicit forbidden list found; "Off the Grid"
20
+ quest forbids cloud APIs *for that badge*. Modal credits provided ($250) but
21
+ using Modal at runtime would forfeit Off the Grid.
22
+ - **Hardware/resources:** Ray-Ban Metas + 2 phones (capture/demo), Mac Studio
23
+ (dev/video), DGX Spark (model eval/fine-tune), $250 Modal + $20 HF credits,
24
+ 40 min/day ZeroGPU via org membership.
25
+ - **Implications:** Space-is-the-product; local small models inside the Space;
26
+ glasses are capture only. Full reasoning in docs/architecture.md.
27
+ - **Open questions:** exact deadline time; video/post content requirements;
28
+ quest claiming mechanism; team status (Topi); Tiny Titan award definition.
29
+
30
+ Evidence: Gmail threads 19e8db3f17a5a437 + 19e9480d1438494e (yuvraj@huggingface.co),
31
+ raw README of build-small-hackathon/README space, org page (all fetched 2026-06-11).
kb/2026-06-11-small-cuts-product-strategy.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ projectId: small-cuts
3
+ id: 2026-06-11-small-cuts-product-strategy
4
+ ---
5
+
6
+ # Small Cuts — Product Strategy (2026-06-11)
7
+
8
+ Canonical detail lives in `docs/product-strategy.md` (same repo). Essence:
9
+
10
+ - **Experience:** an omniscient, slightly-too-honest cinematic narrator for
11
+ moments of your life (*The Invention of Lying*), delivered as a Gradio Space:
12
+ photo/clip in → director style → grounded 2–4 sentence narration → spoken TTS
13
+ → shareable title card. Re-cut with a different director = the delight loop.
14
+ - **What the Director's Cut trajectory got right:** the concept; the Gradio
15
+ pivot; the local-model instinct (Qwen3-Omni).
16
+ - **What it under-exploited:** iOS app is unjudgeable; Gemini Live forfeits
17
+ Off the Grid; glasses-as-infrastructure is demo-fragile; quests unclaimed.
18
+ - **Why this can beat the original:** zero-friction judging, quest multipliers
19
+ (Off the Grid, Off-Brand, Llama Champion, Field Notes, + Well-Tuned stretch),
20
+ honest small-model fit, glasses preserved as the demo-video star.
21
+ - **Core demo flow:** Ray-Ban POV footage → Space narrates it → re-cut in a
22
+ second genre → "everything under 32B, everything in this Space."
23
+ - **Minimum lovable submission:** 6 styles, VLM+TTS local in Space, custom
24
+ theme, examples gallery — live by June 14.
25
+ - **Stretch:** clip input, day reel, published LoRA narrator fine-tune.
26
+ - **Riskiest assumptions:** small-VLM narration specificity; ZeroGPU latency;
27
+ TTS performance quality; submission mechanics.
28
+ - **Milestones:** M0 bootstrap ✅ (today) · M1 real narration (Jun 12) ·
29
+ M2 TTS/theme (Jun 13) · M3 Space live (Jun 13–14) · M4 assets (Jun 14) ·
30
+ M5 submit (Jun 15 morning).
kb/2026-06-11-small-cuts-repo-bootstrap.md ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ projectId: small-cuts
3
+ id: 2026-06-11-small-cuts-repo-bootstrap
4
+ ---
5
+
6
+ # Small Cuts — Repo Bootstrap (2026-06-11)
7
+
8
+ - **What changed:** `macayaven/small-cuts` went from empty to a working
9
+ skeleton on branch `claude/adoring-clarke-49l3uk`: Gradio vertical slice
10
+ (mock backend end-to-end), pluggable inference backends (mock /
11
+ transformers / llama_cpp), style presets + prompt builder, unit tests,
12
+ CI (ruff lint+format, pytest, gitleaks secret scan), full docs set, KB
13
+ note mirrors.
14
+ - **Why:** 4 days to deadline; the judged artifact is a Gradio Space, so the
15
+ fastest path to a strong submission is a disciplined repo whose `app.py`
16
+ deploys to the Space unchanged, with CI guarding quality from commit one.
17
+ - **Evidence / commands:** `uv run pytest` (green), `uv run ruff check .`
18
+ (clean), `SMALL_CUTS_BACKEND=mock python app.py` (UI smoke). CI workflow at
19
+ `.github/workflows/ci.yml` runs on push.
20
+ - **Current status:** M0 complete. Branch pushed; PR + branch protection are
21
+ Carlos-side actions (no admin API in the bootstrap session).
22
+ - **Next action:** M1 — model eval on DGX/Mac (3 candidate VLMs × fixed photo
23
+ set), pick narrator model, tune prompts.
24
+ - **Risks:** narration specificity of small VLMs unvalidated; ZeroGPU latency;
25
+ submission mechanics unverified; canonical KB not yet updated (this note is
26
+ a mirror pending import).
kb/README.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # KB note mirrors
2
+
3
+ The canonical knowledge base (`.knowledge/` + `knowledge_base-*` MCP tools)
4
+ lives on the Mac Studio and was **not reachable from the cloud session that
5
+ bootstrapped this repo** (no knowledge_base MCP server, no `.knowledge/`
6
+ directory, no Tailnet).
7
+
8
+ These markdown files are faithful, import-ready mirrors of the notes that the
9
+ operating contract requires. Each file's front matter carries the intended
10
+ `projectId` and `id`.
11
+
12
+ **Action for the next Mac Studio session:** import each note via
13
+ `knowledge_base-knowledge_save` using the front-matter `projectId`/`id`,
14
+ then keep the canonical KB as source of truth (mirror here may lag).
pyproject.toml ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "small-cuts"
3
+ version = "0.1.0"
4
+ description = "An omniscient cinematic narrator for moments of your life, powered by small open models (Build Small Hackathon)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "gradio>=5.0",
9
+ "pillow>=10.0",
10
+ "pillow-heif>=0.18",
11
+ "av>=12.0",
12
+ # core viewer dep: decodes seed durations + writes generated voice-overs (viewer.py),
13
+ # imported at module scope — must be in the base install (and CI), not just the tts extra.
14
+ "soundfile>=0.12",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ local = [
19
+ "transformers>=4.49",
20
+ "torch>=2.4",
21
+ "torchvision>=0.19",
22
+ "accelerate>=1.0",
23
+ ]
24
+ tts = [
25
+ "kokoro>=0.9",
26
+ "soundfile>=0.12",
27
+ ]
28
+ llama = [
29
+ "llama-cpp-python>=0.3",
30
+ ]
31
+ engine = [
32
+ "fastapi>=0.110",
33
+ "uvicorn>=0.30",
34
+ "websockets>=12",
35
+ "jsonschema>=4.21",
36
+ "soundfile>=0.12",
37
+ "httpx>=0.27",
38
+ ]
39
+ dev = [
40
+ "pytest>=8.0",
41
+ "ruff>=0.8",
42
+ "jsonschema>=4.21",
43
+ ]
44
+
45
+ [build-system]
46
+ requires = ["hatchling"]
47
+ build-backend = "hatchling.build"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/small_cuts"]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+ addopts = "-q"
55
+
56
+ [tool.ruff]
57
+ line-length = 100
58
+ src = ["src", "tests"]
59
+
60
+ [tool.ruff.lint]
61
+ select = ["E", "F", "W", "I", "UP", "B", "SIM"]
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Hugging Face Space runtime for live-demo viewer mode. Gradio + spaces are
2
+ # platform-managed; inference and TTS run on the Mac Studio engine.
3
+ pillow>=10.0
4
+ pillow-heif>=0.18
5
+ av>=12.0
6
+ httpx>=0.27
7
+ soundfile>=0.12
scripts/dress_rehearsal.py ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """End-to-end dress rehearsal: simulate the iOS capture app against a RUNNING engine.
2
+
3
+ Samples real glasses footage into N gated moments, sends contract-valid
4
+ MomentEnvelopes over one ``/v1/session`` WebSocket, validates every frame the
5
+ engine returns against docs/contracts, then audits the persisted library over
6
+ HTTP. Emits a markdown latency report compared against the budget table in
7
+ docs/product/architecture.md.
8
+
9
+ Usage:
10
+ uv run python scripts/dress_rehearsal.py \
11
+ --engine ws://127.0.0.1:8077 \
12
+ --video /path/to/clip.MOV \
13
+ --moments 3 --style symmetrist \
14
+ --report docs/product/rehearsal.md
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import asyncio
21
+ import base64
22
+ import io
23
+ import json
24
+ import statistics
25
+ import sys
26
+ import tempfile
27
+ import time
28
+ import uuid
29
+ import wave
30
+ from dataclasses import dataclass, field
31
+ from datetime import datetime, timedelta, timezone
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ import httpx
36
+ import jsonschema
37
+ import websockets
38
+ from PIL import Image
39
+
40
+ from small_cuts.frames import sample_frames
41
+
42
+ REPO_ROOT = Path(__file__).resolve().parents[1]
43
+ CONTRACTS = REPO_ROOT / "docs" / "contracts"
44
+ CONTRACT_VERSION = "1.1.0"
45
+ MAX_FRAME_SIDE = 1024
46
+ JPEG_QUALITY = 85
47
+ MOMENT_TIMEOUT_S = 600.0 # first moment includes the llama-server cold spawn
48
+ STATUS_IDLE_GRACE_S = 5.0
49
+ PERSIST_GRACE_S = 15.0 # SceneAudio precedes the library write; allow it to land
50
+ WS_MAX_MESSAGE_BYTES = 64 * 1024 * 1024 # SceneAudio WAVs are base64; default 1 MiB is too small
51
+
52
+ # docs/product/architecture.md, "Latency budget v1"
53
+ BUDGET_NARRATION_MS = 4_500
54
+ BUDGET_TTS_MS = 4_000
55
+ BUDGET_E2E_WARM_P50_MS = 10_000
56
+
57
+
58
+ class RehearsalError(RuntimeError):
59
+ """Any contract or pipeline violation - the rehearsal fails loudly."""
60
+
61
+
62
+ def _validator(name: str) -> jsonschema.Draft202012Validator:
63
+ schema = json.loads((CONTRACTS / name).read_text())
64
+ return jsonschema.Draft202012Validator(schema, format_checker=jsonschema.FormatChecker())
65
+
66
+
67
+ MOMENT_VALIDATOR = _validator("moment.schema.json")
68
+ CONTROL_VALIDATOR = _validator("control.schema.json")
69
+ SCENE_AUDIO_VALIDATOR = _validator("scene-audio.schema.json")
70
+ NARRATED_SCENE_VALIDATOR = _validator("narrated-scene.schema.json")
71
+
72
+
73
+ @dataclass
74
+ class MomentResult:
75
+ """Everything observed for one moment: outcome, audio, and timings."""
76
+
77
+ index: int
78
+ moment_id: str
79
+ wall_ms: int
80
+ scene_id: str = ""
81
+ narration: str = ""
82
+ wav_path: Path | None = None
83
+ wav_bytes: bytes = b""
84
+ wav_duration_s: float = 0.0
85
+ error: dict[str, Any] | None = None
86
+ engine_latency_ms: dict[str, int] = field(default_factory=dict)
87
+
88
+ @property
89
+ def ok(self) -> bool:
90
+ return self.error is None
91
+
92
+
93
+ def pick_spread_frames(frames: list[Image.Image], count: int) -> list[Image.Image]:
94
+ """Pick `count` frames spread evenly across the clip (first..last)."""
95
+ if not frames:
96
+ raise RehearsalError("no frames sampled from the video")
97
+ if count <= 0:
98
+ raise RehearsalError(f"--moments must be positive, got {count}")
99
+ if len(frames) < count:
100
+ raise RehearsalError(f"video yielded {len(frames)} sampled frames; need {count}")
101
+ if count == 1:
102
+ return [frames[len(frames) // 2]]
103
+ step = (len(frames) - 1) / (count - 1)
104
+ return [frames[round(i * step)] for i in range(count)]
105
+
106
+
107
+ def downscale(image: Image.Image) -> Image.Image:
108
+ """Longest side <= 1024 px, per the verified Qwen-VL constraint in the contract."""
109
+ image = image.convert("RGB")
110
+ if max(image.size) > MAX_FRAME_SIDE:
111
+ image.thumbnail((MAX_FRAME_SIDE, MAX_FRAME_SIDE), Image.Resampling.LANCZOS)
112
+ return image
113
+
114
+
115
+ def build_envelope(
116
+ image: Image.Image,
117
+ session_id: str,
118
+ style_key: str,
119
+ seq: int,
120
+ captured_at: datetime,
121
+ prev_moment_id: str | None,
122
+ ) -> dict[str, Any]:
123
+ """One contract-valid MomentEnvelope; validation failure here is a harness bug."""
124
+ buffer = io.BytesIO()
125
+ image.save(buffer, format="JPEG", quality=JPEG_QUALITY)
126
+ envelope = {
127
+ "contract_version": CONTRACT_VERSION,
128
+ "moment_id": str(uuid.uuid4()),
129
+ "session_id": session_id,
130
+ "captured_at": captured_at.isoformat(),
131
+ "frames": [
132
+ {
133
+ "jpeg_b64": base64.b64encode(buffer.getvalue()).decode("ascii"),
134
+ "width": image.width,
135
+ "height": image.height,
136
+ }
137
+ ],
138
+ "gate": {"trigger": "interval"},
139
+ "context": {"style_key": style_key, "network": "wifi"},
140
+ "prev_moment_id": prev_moment_id,
141
+ "seq": seq,
142
+ "sent_at": datetime.now(timezone.utc).isoformat(),
143
+ }
144
+ error = jsonschema.exceptions.best_match(MOMENT_VALIDATOR.iter_errors(envelope))
145
+ if error is not None:
146
+ raise RehearsalError(f"harness built a contract-INVALID MomentEnvelope: {error.message}")
147
+ return envelope
148
+
149
+
150
+ def validate_incoming(frame: dict[str, Any]) -> str:
151
+ """Validate one engine frame against the contracts; returns 'control' or 'scene_audio'."""
152
+ if "kind" in frame:
153
+ error = jsonschema.exceptions.best_match(CONTROL_VALIDATOR.iter_errors(frame))
154
+ if error is not None:
155
+ raise RehearsalError(f"contract-invalid ControlFrame from engine: {error.message}")
156
+ return "control"
157
+ error = jsonschema.exceptions.best_match(SCENE_AUDIO_VALIDATOR.iter_errors(frame))
158
+ if error is not None:
159
+ raise RehearsalError(f"contract-invalid SceneAudio from engine: {error.message}")
160
+ return "scene_audio"
161
+
162
+
163
+ def verify_wav(data: bytes, label: str) -> float:
164
+ """RIFF/WAVE magic + nonzero duration; returns duration in seconds."""
165
+ if data[:4] != b"RIFF" or data[8:12] != b"WAVE":
166
+ raise RehearsalError(f"{label}: not a RIFF/WAVE file (magic {data[:12]!r})")
167
+ with wave.open(io.BytesIO(data)) as wav:
168
+ frames = wav.getnframes()
169
+ rate = wav.getframerate()
170
+ if frames <= 0 or rate <= 0:
171
+ raise RehearsalError(f"{label}: zero-duration WAV ({frames} frames @ {rate} Hz)")
172
+ return frames / rate
173
+
174
+
175
+ async def drain_until_idle(ws: websockets.ClientConnection, results: list[str]) -> None:
176
+ """Honor backpressure: after a terminal frame, wait for status busy=false."""
177
+ deadline = time.monotonic() + STATUS_IDLE_GRACE_S
178
+ while time.monotonic() < deadline:
179
+ try:
180
+ raw = await asyncio.wait_for(ws.recv(), timeout=deadline - time.monotonic())
181
+ except (TimeoutError, asyncio.TimeoutError):
182
+ return # engine already idle and silent - safe to proceed
183
+ frame = json.loads(raw)
184
+ validate_incoming(frame)
185
+ if frame.get("kind") == "status":
186
+ results.append(f"status busy={frame['status'].get('busy')}")
187
+ if not frame["status"].get("busy", False):
188
+ return
189
+
190
+
191
+ async def run_moment(
192
+ ws: websockets.ClientConnection,
193
+ envelope: dict[str, Any],
194
+ index: int,
195
+ wav_dir: Path,
196
+ ) -> MomentResult:
197
+ """Send one envelope and await its terminal frame (SceneAudio or error)."""
198
+ moment_id = envelope["moment_id"]
199
+ started = time.perf_counter()
200
+ await ws.send(json.dumps(envelope))
201
+
202
+ acked = False
203
+ while True:
204
+ elapsed = time.perf_counter() - started
205
+ if elapsed > MOMENT_TIMEOUT_S:
206
+ raise RehearsalError(f"moment {index}: no terminal frame after {MOMENT_TIMEOUT_S}s")
207
+ raw = await asyncio.wait_for(ws.recv(), timeout=MOMENT_TIMEOUT_S - elapsed)
208
+ frame = json.loads(raw)
209
+ kind = validate_incoming(frame)
210
+
211
+ if kind == "control":
212
+ if frame["kind"] == "ack":
213
+ if frame["moment_id"] != moment_id:
214
+ raise RehearsalError(f"moment {index}: ack for foreign id {frame['moment_id']}")
215
+ result = frame["ack"]["result"]
216
+ if result != "accepted":
217
+ raise RehearsalError(f"moment {index}: not accepted ({result})")
218
+ acked = True
219
+ print(f" moment {index}: accepted")
220
+ elif frame["kind"] == "error":
221
+ wall_ms = round((time.perf_counter() - started) * 1000)
222
+ print(f" moment {index}: ERROR {frame['error']}")
223
+ return MomentResult(index, moment_id, wall_ms, error=frame["error"])
224
+ # status frames: informational while waiting on our own moment
225
+ continue
226
+
227
+ # SceneAudio: the success terminal frame
228
+ wall_ms = round((time.perf_counter() - started) * 1000)
229
+ if not acked:
230
+ raise RehearsalError(f"moment {index}: SceneAudio arrived before any ack")
231
+ if frame["moment_id"] != moment_id:
232
+ raise RehearsalError(f"moment {index}: SceneAudio for foreign id {frame['moment_id']}")
233
+ wav_bytes = base64.b64decode(frame["audio_b64"])
234
+ duration = verify_wav(wav_bytes, f"moment {index} SceneAudio WAV")
235
+ wav_path = wav_dir / f"moment-{index}-{frame['scene_id']}.wav"
236
+ wav_path.write_bytes(wav_bytes)
237
+ narration = frame.get("narration", "")
238
+ print(f" moment {index}: SceneAudio in {wall_ms} ms ({duration:.1f}s audio)")
239
+ return MomentResult(
240
+ index=index,
241
+ moment_id=moment_id,
242
+ wall_ms=wall_ms,
243
+ scene_id=frame["scene_id"],
244
+ narration=narration,
245
+ wav_path=wav_path,
246
+ wav_bytes=wav_bytes,
247
+ wav_duration_s=duration,
248
+ )
249
+
250
+
251
+ async def websocket_phase(
252
+ engine_ws: str, envelopes: list[dict[str, Any]], wav_dir: Path
253
+ ) -> list[MomentResult]:
254
+ """One session socket; moments sent sequentially for clean latency numbers."""
255
+ ws_url = f"{engine_ws.rstrip('/')}/v1/session"
256
+ print(f"connecting {ws_url}")
257
+ results: list[MomentResult] = []
258
+ status_log: list[str] = []
259
+ async with websockets.connect(ws_url, max_size=WS_MAX_MESSAGE_BYTES) as ws:
260
+ for index, envelope in enumerate(envelopes, start=1):
261
+ results.append(await run_moment(ws, envelope, index, wav_dir))
262
+ if index < len(envelopes):
263
+ await drain_until_idle(ws, status_log)
264
+ return results
265
+
266
+
267
+ def http_phase(
268
+ http_base: str, session_id: str, results: list[MomentResult]
269
+ ) -> list[dict[str, Any]]:
270
+ """Verify persistence: /v1/scenes lists contract-valid scenes; media bytes match."""
271
+ ok_results = [r for r in results if r.ok]
272
+ with httpx.Client(base_url=http_base, timeout=30.0) as client:
273
+ # SceneAudio is sent to the client BEFORE the library sink commits, so
274
+ # the final write may still be in flight — poll briefly before failing.
275
+ deadline = time.monotonic() + PERSIST_GRACE_S
276
+ while True:
277
+ response = client.get("/v1/scenes", params={"session": session_id})
278
+ response.raise_for_status()
279
+ scenes = response.json()["scenes"]
280
+ if len(scenes) >= len(ok_results) or time.monotonic() > deadline:
281
+ break
282
+ time.sleep(0.25)
283
+ if len(scenes) != len(ok_results):
284
+ raise RehearsalError(
285
+ f"library has {len(scenes)} scenes for session {session_id}; "
286
+ f"expected {len(ok_results)}"
287
+ )
288
+ by_id = {scene["scene_id"]: scene for scene in scenes}
289
+ for result in ok_results:
290
+ scene = by_id.get(result.scene_id)
291
+ if scene is None:
292
+ raise RehearsalError(f"scene {result.scene_id} missing from /v1/scenes")
293
+ error = jsonschema.exceptions.best_match(NARRATED_SCENE_VALIDATOR.iter_errors(scene))
294
+ if error is not None:
295
+ raise RehearsalError(
296
+ f"scene {result.scene_id} is contract-INVALID: {error.message}"
297
+ )
298
+ result.engine_latency_ms = scene["engine"]["latency_ms"]
299
+
300
+ # One media spot-check: the frame JPEG and the voice WAV must serve real bytes.
301
+ probe = ok_results[0]
302
+ jpg = client.get(f"/media/{probe.scene_id}/frame.jpg")
303
+ jpg.raise_for_status()
304
+ if jpg.content[:2] != b"\xff\xd8":
305
+ raise RehearsalError(f"/media frame.jpg is not a JPEG (magic {jpg.content[:4]!r})")
306
+ wav = client.get(f"/media/{probe.scene_id}/voice.wav")
307
+ wav.raise_for_status()
308
+ verify_wav(wav.content, "/media voice.wav")
309
+ if wav.content != probe.wav_bytes:
310
+ raise RehearsalError("/media voice.wav differs from the WS-delivered SceneAudio WAV")
311
+ print(
312
+ f"library check: {len(scenes)} scenes persisted; media verified "
313
+ f"(frame.jpg {len(jpg.content)} B, voice.wav {len(wav.content)} B)"
314
+ )
315
+ return scenes
316
+
317
+
318
+ def _check(value: int | None, budget: int) -> str:
319
+ if value is None:
320
+ return "n/a"
321
+ return "PASS" if value <= budget else "FAIL"
322
+
323
+
324
+ def build_report(
325
+ args: argparse.Namespace,
326
+ session_id: str,
327
+ results: list[MomentResult],
328
+ scenes: list[dict[str, Any]],
329
+ ) -> str:
330
+ """Markdown report: per-moment table, budget comparison, full narrations."""
331
+ engine_meta = scenes[0]["engine"] if scenes else {}
332
+ lines = [
333
+ f"# Dress rehearsal — {datetime.now(timezone.utc).date().isoformat()}",
334
+ "",
335
+ f"- Engine: `{args.engine}` (session `{session_id}`)",
336
+ f"- Video: `{args.video}` — {args.moments} gated moments, style `{args.style}`",
337
+ f"- Narrator: `{engine_meta.get('narrator_model', '?')}` "
338
+ f"via `{engine_meta.get('narrator_backend', '?')}`",
339
+ f"- TTS: `{engine_meta.get('tts_model', '?')}`",
340
+ "",
341
+ "## Per-moment latency",
342
+ "",
343
+ "Engine numbers come from the persisted scene's `engine.latency_ms`; wall is the",
344
+ "client-measured send→SceneAudio round trip (includes WS transfer + queueing).",
345
+ "",
346
+ "| # | Narration (first 80 chars) | narration_ms | tts_ms | total_ms (engine) "
347
+ "| wall_ms (client) | narration ≤4500 | tts ≤4000 |",
348
+ "|---|---|---|---|---|---|---|---|",
349
+ ]
350
+ for r in results:
351
+ if not r.ok:
352
+ err = r.error or {}
353
+ lines.append(
354
+ f"| {r.index} | ERROR {err.get('stage')}/{err.get('code')}: "
355
+ f"{str(err.get('message', ''))[:60]} | — | — | — | {r.wall_ms} | FAIL | FAIL |"
356
+ )
357
+ continue
358
+ narration_ms = r.engine_latency_ms.get("narration")
359
+ tts_ms = r.engine_latency_ms.get("tts")
360
+ total_ms = r.engine_latency_ms.get("total")
361
+ excerpt = r.narration[:80].replace("|", "\\|")
362
+ lines.append(
363
+ f"| {r.index} | {excerpt} | {narration_ms} | {tts_ms} | {total_ms} "
364
+ f"| {r.wall_ms} | {_check(narration_ms, BUDGET_NARRATION_MS)} "
365
+ f"| {_check(tts_ms, BUDGET_TTS_MS)} |"
366
+ )
367
+
368
+ ok = [r for r in results if r.ok]
369
+ # Moment 1 absorbs the cold start (llama-server spawn + Kokoro weight load);
370
+ # the architecture budgets are explicitly for WARM stages.
371
+ warm = ok[1:] if len(ok) > 1 else ok
372
+ p50_all = round(statistics.median(r.wall_ms for r in ok)) if ok else None
373
+ p50_warm = round(statistics.median(r.wall_ms for r in warm)) if warm else None
374
+ warm_narration = max((r.engine_latency_ms.get("narration", 0) for r in warm), default=None)
375
+ warm_tts = max((r.engine_latency_ms.get("tts", 0) for r in warm), default=None)
376
+ cold = ok[0] if ok else None
377
+ lines += [
378
+ "",
379
+ "## Budget comparison (docs/product/architecture.md, Latency budget v1)",
380
+ "",
381
+ "Warm = moments after the first; moment 1 pays the llama-server spawn and",
382
+ "Kokoro weight load once per engine process.",
383
+ "",
384
+ "| Budget line | Target | Measured (warm max) | Verdict |",
385
+ "|---|---|---|---|",
386
+ f"| Narration, warm llama.cpp | ≤ {BUDGET_NARRATION_MS} ms | {warm_narration} ms "
387
+ f"| {_check(warm_narration, BUDGET_NARRATION_MS)} |",
388
+ f"| TTS, warm Kokoro | ≤ {BUDGET_TTS_MS} ms | {warm_tts} ms "
389
+ f"| {_check(warm_tts, BUDGET_TTS_MS)} |",
390
+ f"| End-to-end warm p50 (client wall) | ≤ {BUDGET_E2E_WARM_P50_MS} ms | {p50_warm} ms "
391
+ f"| {_check(p50_warm, BUDGET_E2E_WARM_P50_MS)} |",
392
+ "",
393
+ ]
394
+ if cold is not None:
395
+ lines.append(
396
+ f"Cold first moment: narration {cold.engine_latency_ms.get('narration')} ms, "
397
+ f"tts {cold.engine_latency_ms.get('tts')} ms, "
398
+ f"wall {cold.wall_ms} ms (one-time per engine process)."
399
+ )
400
+ lines += [
401
+ f"All-moments e2e p50 including the cold first moment: {p50_all} ms.",
402
+ f"Moments narrated: {len(ok)}/{len(results)}; "
403
+ f"audio: {', '.join(f'{r.wav_duration_s:.1f}s' for r in ok)}.",
404
+ "",
405
+ "## Full narrations",
406
+ "",
407
+ ]
408
+ for r in results:
409
+ if r.ok:
410
+ lines += [f"**Moment {r.index}** (scene `{r.scene_id}`):", "", f"> {r.narration}", ""]
411
+ else:
412
+ lines += [f"**Moment {r.index}**: pipeline error `{r.error}`", ""]
413
+ return "\n".join(lines)
414
+
415
+
416
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
417
+ parser = argparse.ArgumentParser(description=__doc__)
418
+ parser.add_argument("--engine", default="ws://127.0.0.1:8077", help="engine WS base URL")
419
+ parser.add_argument("--video", required=True, help="source video (real glasses footage)")
420
+ parser.add_argument("--moments", type=int, default=3, help="gated moments to send")
421
+ parser.add_argument("--style", default="symmetrist", help="director style_key")
422
+ parser.add_argument("--report", default="", help="markdown report path (also printed)")
423
+ return parser.parse_args(argv)
424
+
425
+
426
+ def main(argv: list[str] | None = None) -> int:
427
+ args = parse_args(argv)
428
+ video = Path(args.video)
429
+ if not video.is_file():
430
+ raise RehearsalError(f"video not found: {video}")
431
+ http_base = args.engine.rstrip("/").replace("ws://", "http://").replace("wss://", "https://")
432
+ session_id = f"rehearsal-{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}"
433
+
434
+ print(f"sampling frames from {video.name} ...")
435
+ frames = sample_frames(video, every_n_seconds=2.0)
436
+ picked = pick_spread_frames(frames, args.moments)
437
+ print(f"sampled {len(frames)} frames; picked {len(picked)} spread-out moments")
438
+
439
+ base_capture = datetime.now(timezone.utc)
440
+ envelopes: list[dict[str, Any]] = []
441
+ prev_id: str | None = None
442
+ for seq, frame in enumerate(picked):
443
+ envelope = build_envelope(
444
+ downscale(frame),
445
+ session_id=session_id,
446
+ style_key=args.style,
447
+ seq=seq,
448
+ captured_at=base_capture + timedelta(seconds=seq),
449
+ prev_moment_id=prev_id,
450
+ )
451
+ prev_id = envelope["moment_id"]
452
+ envelopes.append(envelope)
453
+ print(f"built {len(envelopes)} contract-valid MomentEnvelopes (session {session_id})")
454
+
455
+ wav_dir = Path(tempfile.mkdtemp(prefix="dress-rehearsal-"))
456
+ results = asyncio.run(websocket_phase(args.engine, envelopes, wav_dir))
457
+ scenes = http_phase(http_base, session_id, results)
458
+
459
+ report = build_report(args, session_id, results, scenes)
460
+ print("\n" + report)
461
+ if args.report:
462
+ report_path = Path(args.report)
463
+ report_path.parent.mkdir(parents=True, exist_ok=True)
464
+ report_path.write_text(report + "\n")
465
+ print(f"\nreport written to {report_path}")
466
+
467
+ return 0 if all(r.ok for r in results) else 1
468
+
469
+
470
+ if __name__ == "__main__":
471
+ try:
472
+ sys.exit(main())
473
+ except RehearsalError as exc:
474
+ print(f"DRESS REHEARSAL FAILED: {exc}", file=sys.stderr)
475
+ sys.exit(2)
scripts/hooks/block-secrets.sh ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # PreToolUse guard — block edits/writes to secrets files (Small Cuts no-secrets policy).
3
+ # Receives the hook payload as JSON on stdin; exit code 2 blocks the tool and shows stderr.
4
+ f="$(python3 -c 'import json,sys
5
+ try:
6
+ d = json.load(sys.stdin)
7
+ except Exception:
8
+ sys.exit(0)
9
+ print((d.get("tool_input") or {}).get("file_path", "") or "")' 2>/dev/null)"
10
+ b="$(basename -- "$f" 2>/dev/null)"
11
+ case "$b" in
12
+ .env | .env.* | *.env | op-connect.env | op-service-account.env | *.pem | *.key | .npmrc)
13
+ echo "🚫 Blocked edit to '$f' — that looks like a secrets file. Small Cuts keeps secrets in 1Password Connect, never in the repo (gitleaks would also fail CI)." >&2
14
+ exit 2
15
+ ;;
16
+ esac
17
+ exit 0