9.1 KiB
Atomic Events — Design Spec
Status: Stage A + Stage B implemented (2026-07-27). Motivated by a future Glitch Studio GUI that edits each video occurrence as a self-contained object.
Implemented:
- Per-occurrence presentation resolves via
transformer.resolve_video_presentation(precedence: inline/GUI override > shorthand prefix > videos.json > default; videoend_ondefault =next_video,[narration:]runs to end). - events.json is materialized/atomic:
derive_eventswriteshandle/cutout/layer/end_on/ take;events_to_marker_timingsround-trips them as overrides. - Inline grammar
[prefix:handle, key=value, …](parser.parse_marker), threaded through alignment intoMarkerTiming.overrides. Supported inline keys: cutout, layer, end_on, take, volume (numeric keystake/volumecoerced to float). Unknown keys are ignored. - The key-reuse collision validator hard-error was removed (reuse is legal now).
volume is sparse/overridable: the renderer reads VideoEvent.volume (line ~1571),
which is the events.json override if present else the videos.json default — so a videos.json
change keeps propagating, and events.json only stores volume when it's actually overridden
(inline/GUI/manual). derive_events materializes it only when overridden; _EVENT_OVERRIDE_KEYS
round-trips it. This is the template for the remaining globals.
Deferred (follow-ups): inline/per-event override of the other globals (skip/zoom/
use_audio_channels/pause_narration) — same renderer-plumbing pattern as volume, per site;
per-segment narration voiceover volume (render currently uses narration_videos[0].volume
only); stripping the moved fields from videos.json (kept as fallback defaults); a validator
warning for unknown inline keys.
Problem
Presentation/timing properties (cutout, layer, end_on, take, pause_narration)
live on the videos.json handle, but they are really properties of where a clip is
used, not of the file. The shorthand prefix (vst: = square/above, vsb: =
square/below) is per-marker, but _project_markers_to_videos collapses it onto the
single handle record (last-wins). So one handle used two ways collides:
[vst:glitch_ccd_binning](above) and[vsb:glitch_ccd_binning](below) → videos.json can only storelayer: below, so the first occurrence renders under the slide (hidden).- video5 has 5 such collisions today (glitch_ccd_binning, pexels/12471039…, mainvideopart1, shotnoiseacc, slide_periodogram).
A stopgap validator hard-error (validate_project, gnommo/validator.py) currently blocks
render on these. This spec removes the cause so that guard is no longer needed.
The naive fixes are both rejected: copying the file/handle (duplication on disk), and a "hybrid override + materialize" layer (too much indirection). Instead: the per-occurrence properties move onto the event.
Field homes
videos.json — asset + global defaults (one value per handle):
source_file, output_file/processed_file, filter, has_audio, is_shared,
src_mtime, duration (probed; asset-only, never per-event), and the globals
zoom, skip, volume, use_audio_channels.
events.json — per-occurrence (one value per event):
handle (the video id, prefix-free), cutout, layer, end_on, take,
pause_narration.
Resolution order for a rendered clip: event field (if set) → videos.json value (for the globals) → config default. The per-occurrence fields have no videos.json fallback — they are always materialized onto the event at build time.
Notes:
end_ondefaults tonext_videofor videos when unspecified (was implicitlynext_slide). Existing videos.jsonend_onvalues are migrated onto events explicitly, so current projects keep their behavior; only new unspecified markers get the new default.takeis the event-level cut length, only meaningful whenend_on=take; otherwise the end is implicit fromend_onandtakestays null.skipstays a global (asset trim-in) whiletakeis per-event — a deliberate asymmetry: "where this asset generally starts" vs. "how long this occurrence plays."zoom/volume/use_audio_channelsstay global but are inline-overridable per event (below), so they can diverge without a videos.json copy.
Authoring: shorthand + inline overloads
The manuscript stays the compact authoring surface. The shorthand letters encode
cutout+layer (and pause_narration via the …p: variants). Anything the letters
don't encode — chiefly end_on, and any per-event override of a global — is given as
inline key=value pairs (simplified from the earlier {"json":"form"}):
[vsb:glitch_ccd_binning2] # square/below, end_on defaults to next_video
[vsb:glitch_ccd_binning2, end_on=next_video] # + explicit end_on
[vsb:glitch_ccd_binning2, take=5, volume=0.5] # + per-event overrides of globals
[video:glitch_ccd_binning2, cutout=square, layer=below] # generic; equivalent to [vsb:…]
Rules:
- The first token inside
[]isprefix:handle(handle may contain/, e.g.pexels/123). - Remaining comma-separated tokens are
key=value. Values are type-inferred: numeric → float,true/false→ bool, else string. Allowed keys:cutout,layer,end_on,take,skip,zoom,volume,use_audio_channels,pause_narration,always_visible. - An inline key overrides whatever the shorthand implied (e.g.
[vst:x, layer=below]→ above from the prefix, then below from the override). Last-writer-wins, prefix first. [video:handle, …]is the fully-explicit form the GUI round-trips: no prefix magic, every presentation field named.
Why key=value over JSON: no braces/quotes to escape inside [], one obvious separator,
and it reads cleanly in a script. The GUI still stores the resolved values as real JSON
fields on the event — the manuscript form is just sugar that populates them.
Build-time materialization
At build (build_render_plan / scaffold construction), each video marker resolves to an
atomic event dict:
{
"type": "video",
"handle": "glitch_ccd_binning",
"cutout": "square",
"layer": "above",
"end_on": "next_video",
"take": null,
"pause_narration": 0.0,
"narration_time": 0.0, "adjustment": 0.0, "final_time": 0.0,
"mapping": "exact", "confidence": 1.0, "context": "…"
}
id (currently "vst:glitch_ccd_binning") is replaced by handle + explicit fields. The
render pass reads presentation straight off the event and no longer consults the prefix or
the videos.json presentation fields. merge_events must preserve manual event edits (the
GUI's writes) across rebuilds, the same way it preserves adjustment today.
Code touchpoints
- models.py —
VideoSourceshedscutout/layer/end_on/take/pause_narration(or they become defaults-only);VideoEventalready carriescutout/layer/end_on— extend totake/pause_narrationsourced from the event, not the handle. - parser.py
parse_manuscript— extend the marker grammar to acceptprefix:handle, key=value, …; update the malformed-marker detector (which today flags spaces/commas inside[]). - transformer.py
_extract_video_events— resolvecutout/layer/end_on/take/ pause_narrationfrom (prefix ∪ inline overrides), not fromvideo_source. - scaffold.py — event schema:
handle+ presentation fields;merge_eventspreserves GUI edits; migration for existing events.json. - cli.py — retire
_project_markers_to_videosand_writeback_video_metadata(they project/writeback per-handle presentation) in favor of seeding event fields. - validator.py — remove the key-reuse collision hard-error (reuse is legal now).
- renderer.py — read presentation from the event (mostly already does via
VideoEvent).
Migration
Existing projects (video0–video6, …) have presentation on the handle and prefixed ids in
events.json. A one-shot migration, run on build:
- For each video event, split the prefixed
idintohandle+ impliedcutout/layer. - Fill
end_on/take/pause_narrationfrom the handle's current videos.json values (preserving today's behavior — including handles that explicitly setnext_slide). - Strip the moved fields from videos.json handles (leave the globals).
- Idempotent: a second run is a no-op once events carry
handle.
Staging
- Stage A — schema split + per-event resolution from the shorthand prefix, migration, remove the collision guard. Shorthand-only authoring keeps working; the 5 video5 collisions resolve. (This is the part that fixes the bug.)
- Stage B — the inline
key=valueoverload grammar + malformed-marker updates.
Keep the validator collision hard-error in place until Stage A lands — removing it earlier would let the hidden-overlay bug back in on video5.
Open questions
always_visible,use_audio_channels: confirmed as inline-overridable globals — do any need to become fully per-event?- Does the GUI want events fully flattened (every field present) or sparse (only overrides, inherit the rest)? Affects whether the build writes defaults explicitly.