# 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; video `end_on` default = `next_video`, `[narration:]` runs to end). - events.json is materialized/atomic: `derive_events` writes `handle/cutout/layer/end_on/ take`; `events_to_marker_timings` round-trips them as overrides. - Inline grammar `[prefix:handle, key=value, …]` (`parser.parse_marker`), threaded through alignment into `MarkerTiming.overrides`. Supported inline keys: **cutout, layer, end_on, take, volume** (numeric keys `take`/`volume` coerced 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 store `layer: 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_on` **defaults to `next_video`** for videos when unspecified (was implicitly `next_slide`). Existing videos.json `end_on` values are migrated onto events explicitly, so current projects keep their behavior; only *new* unspecified markers get the new default. - `take` is the event-level cut length, only meaningful when `end_on=take`; otherwise the end is implicit from `end_on` and `take` stays null. - `skip` stays a global (asset trim-in) while `take` is per-event — a deliberate asymmetry: "where this asset generally starts" vs. "how long this occurrence plays." - `zoom`/`volume`/`use_audio_channels` stay 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 `[]` is `prefix: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: ```json { "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** — `VideoSource` sheds `cutout`/`layer`/`end_on`/`take`/`pause_narration` (or they become defaults-only); `VideoEvent` already carries `cutout`/`layer`/`end_on` — extend to `take`/`pause_narration` sourced from the event, not the handle. - **parser.py `parse_manuscript`** — extend the marker grammar to accept `prefix:handle, key=value, …`; update the malformed-marker detector (which today flags spaces/commas inside `[]`). - **transformer.py `_extract_video_events`** — resolve `cutout/layer/end_on/take/ pause_narration` from (prefix ∪ inline overrides), not from `video_source`. - **scaffold.py** — event schema: `handle` + presentation fields; `merge_events` preserves GUI edits; migration for existing events.json. - **cli.py** — retire `_project_markers_to_videos` and `_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 `id`s in events.json. A one-shot migration, run on build: 1. For each video event, split the prefixed `id` into `handle` + implied `cutout`/`layer`. 2. Fill `end_on`/`take`/`pause_narration` from the handle's current videos.json values (preserving today's behavior — including handles that explicitly set `next_slide`). 3. Strip the moved fields from videos.json handles (leave the globals). 4. 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=value` overload 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.