Skip to content

Blender swatch scenes, and a 29th style: tabletop-miniature - #42

Merged
ZLHad merged 6 commits into
mainfrom
claude/tabletop-miniature
Oct 2, 2026
Merged

ZLHad merged 6 commits into
mainfrom
claude/tabletop-miniature

Conversation

@ZLHad

@ZLHad ZLHad commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

What

  1. Style swatches can be Blender scenes. A style folder with swatch.py instead of swatch.js renders in Blender 5.2 (Cycles) through styles/_swatch/render.sh.
    • The PNG frames become the same 1080p BT.709 hf.mp4 the HyperFrames path makes, so the frame checks, music, foley, mix, bin/vh qa, encode, poster and sheet are all shared.
    • New pieces: blender_render.py (runs inside Blender: build(env) once, apply(t, env) per frame), blender_prep.py (a static check of the scene and font resolution), and Blender branches in render.sh, determinism.sh, foley.mjs and bin/vh style.
  2. A 29th style, tabletop-miniature (桌面微缩剧场): stop-motion miniature on a real-scale tabletop.
    • The full package: STYLE.md, tokens, a 5 s swatch, a one-piano score, and 7 synthesized foley sounds.
    • Learned from Aardman's A Grand Day Out, Laika's Coraline, Olivo Barbieri's tilt-shift Site Specific, and the Kevin Ngo piano film this started from.
  3. engines/blender.md goes from "experimental, never run" to "partly verified", with the first-smoke results measured on Blender 5.2.2.

Licence

As decided in the session, files that import bpy are GPL-3.0-or-later with an SPDX header: styles/_swatch/blender_render.py and styles/tabletop-miniature/swatch.py. Everything else stays MIT. Both READMEs' licence sections and engines/blender.md record this. tools/ci.sh fails a .py that imports bpy without the header, and runs the static check on every styles/*/swatch.py.

Safety (following engines/blender.md)

Three layers. The third is the boundary; the first is a lint.

  • Static check first: blender_prep.py scan reads swatch.py as a syntax tree before anything runs it.
    • Imports are allow-listed: bpy bmesh mathutils math colorsys json sys, which rules out random and time too. Of json only dumps/loads, of sys only argv.
    • Names and attributes starting with _, and strings containing __, are refused. That closes the dunder routes the code review found (bpy.utils._os, x.__dict__["__import__"]).
    • Also refused: eval exec compile open getattr setattr type dir globals vars, and bpy that loads, saves or runs anything (.blend files, text blocks, drivers, bpy.utils, save_render, handlers, timers).
    • foley.mjs runs the same scan before it reads FOLEY, and on macOS reads it inside a sandbox of its own.
  • Clean environment: Blender runs under env -i, so no API keys reach it. Its HOME and TMPDIR are in out/<slug>/.blender/.
  • Sandbox on macOS (sandbox-exec), measured on Blender 5.2.2:
    • no network, no handing URLs or Apple events to other apps;
    • writes only to out/<slug>/ and Blender's own subfolder of the per-user cache. Metal keeps compiled shaders there; with it denied, every frame recompiles (13 s instead of 2 s). The temp folder is not needed.
    • final (CPU) renders also cannot read the home folder, except the repo, Blender and ~/Library/Fonts. Frame 90 rendered under that rule is pixel-identical to the frame rendered without it.
    • drafts (Metal) cannot take the home-folder rule: Metal crashes (SIGSEGV in -[_MTLDevice recordBinaryArchiveUsage:]) loading cached kernels when home is unreadable, even with ~/Library allowed. Drafts stay offline and write only to the git-ignored out/.
    • Linux has no sandbox: render only scenes you have read.
  • A cold Metal kernel compile is silent for about 110 s, so the stall watchdog allows 300 s for Blender scenes. When one of several Blender workers fails, the others are stopped.

Determinism

  • Device: final renders use Cycles on the CPU. The same frame rendered twice is pixel-identical on the CPU, but not on Metal (92 dB) or EEVEE (85 dB). Drafts use Metal.
  • The check: for a Blender scene, determinism.sh re-renders 12 frames spread over the clip, last to first, in 3 fresh Blender processes. It compares them pixel by pixel with the final in-order render. The final render's frames are reused when the stamp in their inputs.txt (a checksum of the scene) still matches.
  • It caught a real bug. On the first final render, render.use_persistent_data made Cycles on the CPU draw the teacup black on frames 27–44 of the in-order render; those frames rendered alone were grey. Frame 30 failed at 24.5 dB.
    • Fix: persistent data is now off. Per-frame time barely changed.
    • The final swatch passes: 12/12 identical.

How it was checked

  • CI: tools/ci.sh --committed passes on the merge with main (Case study: five community films, concept before style #43–Research note 06: concept-first against floors only; two self-checks for quick #46), under both bash 5 and macOS bash 3.2. Shellcheck 0.11 and pyflakes are clean.

  • Swatch render:

    • 150 frames at 15–19 s each on an M3 Max: CPU, 32 spp + OIDN. 64 spp looked the same side by side at 32–43 s per frame.
    • swatch.mp4 is 1.14 MB and poster.jpg 67 KB, both under the caps.
    • Audio QA passes on the WAV and the mp4: 26 cues, all within one frame (median 5.3 ms), no digital silence, no pumping.
    • determinism.sh: 12/12 frames pixel-identical (last to first, 3 fresh processes).
  • Layout: checked frame by frame with a pinhole projection that matches the Blender camera, before rendering. The hanging card clears the highest hop by 20 px; text stays inside the safe area through every camera move (title text x ≥ 239, y ≥ 105); the end frame's blocks sit above the action-safe line.

  • Gallery: rebuilt with 29 styles (jpg 1.06 MB, mp4 7.2 MB).

  • Review: seven rounds of independent "harsh motion director" review on fresh contexts. The lowest score was 6, then 7 in every round; no round had all seven dimensions at 8. Scores are hook, phone, motion, variety, polish, accuracy, sync.

    Round Scores Fixed after it
    4 8 8 7 8 7 8 9 last hop on ones; moonlight fill and a dark window frame; a half-lid and a nod before sleep
    5 8 8 7 8 8 9 9 the small hop is crouch, rise, apex, fall (it was one pose held 4 frames); the puck flinches at the lamp-off and the camera keeps moving in the dark; the opening starts with the blocks out of frame and the card comes in after the pull-back
    6 8 8 8 7 7 9 9 the camera cranes up and swings round the set for the payoff; the stitched eyes lie on the round face (they floated off it and showed past the edge); the moon sits higher so its glint on the desk is no longer the brightest thing in frame
    7 (shipped) 8 8 8 7 8 9 8 not fixed, see below

    Round 7's open points, written into styles/README.md and the CHANGELOG:

    • variety: the swing moves the background more than the set, so 3, 4 and 5 s read as one composition. Its suggestion is to push in at the lamp-off and end on a medium close-up against the morning window, at the cost of the title leaving the frame. That is a choice for the maintainer.
    • the blocks are on screen from 0.5 s, before the title (a deviation from the content spec since round 5);
    • light: the desk glint is still the brightest thing in the lower half until the lamp-off; a strip beside 初版 clips at dawn; 初版 is about 2.3:1 in moonlight;
    • sound: the dawn riser starts late and the bird sits under the piano.
  • STYLE.md (the maintainer asked not to write the rules too rigidly): the taste sections describe what the swatch does, why, and when to do otherwise; "禁止项" became "容易翻车的地方", each with its cost. The floors stay hard: frames are pure functions of t, type size, sandbox and key isolation, no digital silence, licences.

Not in this PR

  • A Blender scene cannot read a beat map yet: the static check forbids open, so the renderer would have to pass it in through env. This matters for music: a note map for pictures driven note by note #41's note map: a Blender scene driven note by note would need that.
  • engines/blender.md still lists first-smoke items not run: the EEVEE 300-frame memory test, OIDN flicker on/off, rigid-body cache order, HyperFrames video alpha, PNG metadata, and the two-stage -a command lines.

ZLHad and others added 6 commits October 2, 2026 03:16
styles/_swatch renders a style folder that has swatch.py instead of
swatch.js in Blender (Cycles): blender_render.py runs the scene's
build(env) once and apply(t, env) before every frame, and render.sh turns
the PNG frames into the same 1080p BT.709 hf.mp4 as the HyperFrames path,
so the checks, music, foley, mix, qa, encode and poster are shared. Final
renders use the CPU (pixel-identical run to run; Metal 92 dB, EEVEE 85 dB),
drafts the GPU, with persistent data off (on, it carried state between
frames: a black teacup that determinism.sh caught).

Safety per engines/blender.md: blender_prep.py scans swatch.py as a syntax
tree first (allow-listed imports, no eval/exec/open, .blend IO, handlers,
timers), then Blender runs under env -i and, on macOS, sandbox-exec (no
network; writes to out/<slug>/ and the per-user cache and temp folders that
Metal needs). Fonts resolve through fc-match; a .ttc face Blender cannot
load is written out to out/. determinism.sh re-renders 12 frames out of
order in fresh processes against the final frames. Files that import bpy
are GPL-3.0-or-later with an SPDX header (the maintainer's decision); CI
checks the header and runs the scan.

tabletop-miniature (桌面微缩剧场): stop-motion miniature on a real-scale
desk, practical light as the clock, a felt puppet on twos (on ones while
the camera follows and for the fastest hop), one piano with each action a
note. Four rounds of independent review; engines/blender.md now records
the first smoke results on Blender 5.2.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ter STYLE.md

Swatch (final render, determinism 12/12):
- opening starts with the blocks out of frame; the card comes in after the pull-back
- the small hop is a crouch, rise, apex and fall; the puck flinches at the lamp-off
- the camera cranes up and swings round the set for the payoff, drifts through the
  dark and hands over to the dawn at the same speed
- stitched eyes lie on the round face; the moon sits higher so its desk glint falls
  below the frame; brighter moon fill

Safety, after the code review:
- the scan refuses underscore names and attributes and strings with __, getattr and
  friends, and bpy that loads, saves or runs anything; it is a lint, the sandbox is the
  boundary
- sandbox: no lsopen or Apple events; writes only to out/<slug>/ and Blender's cache
  subfolder; final (CPU) renders cannot read the home folder (Metal crashes with that
  rule, so drafts keep it off)
- foley.mjs scans and sandboxes its read of FOLEY; CI checks the GPL header by AST
- the render stamp includes the Blender version and is taken before rendering; a
  failing worker stops the others; Blender's stall limit is 300 s

STYLE.md: the taste sections are defaults with reasons, not prohibitions.
…ture

# Conflicts:
#	AGENTS.md
#	CHANGELOG.md
#	CLAUDE.md
…ender's real path for the sandbox

From the code review's re-check of #42:
- bl_run execs sandbox-exec (or Blender), so the pid bl_render kills is Blender, not a
  subshell around it; a stub run leaves nothing behind
- the home read-deny's allow rule resolves Blender's real path and allows only its .app
  bundle or install folder; a folder that holds the home folder is refused
- the sandbox profile lives in a temp folder outside out/<slug>/, and on macOS Blender
  does not start without it
- foley.mjs shows the scan's findings and runs the interpreter it resolved, with its
  install readable (pyenv, uv under home)
- the scan refuses `import json as x` and `import sys as x`
- determinism.sh stamps a fresh full render before rendering; CI reads paths by line
…d on any exit; dotted json/sys aliases

- bl_profile runs in the parent before run_watched; cleanup() removes its temp folder,
  so a timeout or Ctrl-C leaves nothing in TMPDIR (stub runs: failure and timeout both
  leave no Blender and no folder)
- the scan flags `import json.decoder as d` too
- styles/_swatch/README.md: how the Blender allow path is chosen, the Homebrew wrapper
  case, where the profile lives
@ZLHad
ZLHad marked this pull request as ready for review October 2, 2026 05:08
@ZLHad
ZLHad merged commit b072f96 into main Oct 2, 2026
2 checks passed
@ZLHad
ZLHad deleted the claude/tabletop-miniature branch October 2, 2026 05:08
ZLHad added a commit that referenced this pull request Oct 4, 2026
- Style swatches can be Blender scenes (swatch.py): build(env) once, apply(t, env) per frame, Cycles on the CPU for final renders (pixel-identical across runs), Metal for drafts. The frames feed the same hf.mp4, checks, music, foley, mix, qa, encode and poster as the HyperFrames path.
- Safety in three layers: an AST scan (a lint), env -i, and on macOS a sandbox-exec profile (no network, no URL or Apple-event hand-off, writes only to out/<slug>/ and Blender's cache folder; final renders cannot read the home folder). Drafts on Metal cannot take the home read-deny (Metal crashes), so they keep it off. Linux has no sandbox.
- determinism.sh for Blender scenes: 12 frames, last to first, in 3 fresh processes, against the final render's frames, using a stamp taken before rendering. It caught a real bug (persistent data drew the teacup black on frames 27-44).
- A 29th style, tabletop-miniature (桌面微缩剧场): stop-motion miniature on a real-scale desk, one upright piano, seven synthesized foley sounds. Seven independent review rounds; shipped at hook 8, phone 8, motion 8, variety 7, polish 8, accuracy 9, sync 8, with the open points in styles/README.md.
- STYLE.md writes taste as defaults with reasons; the floors stay hard.
- Files that import bpy are GPL-3.0-or-later with an SPDX header (the maintainer's decision); CI checks it. engines/blender.md is now "partly verified".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant