Files
gnommo/docs/atomic_events.md

9.1 KiB
Raw Permalink Blame History

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:

{
  "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.pyVideoSource 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.pyremove 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 (video0video6, …) have presentation on the handle and prefixed ids 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.