Adding fixes to the render system

This commit is contained in:
2026-07-27 19:16:02 +02:00
parent cdda2e9024
commit 0c8f662bee
6 changed files with 410 additions and 62 deletions
+165
View File
@@ -0,0 +1,165 @@
# 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** (the fully-wired per-event fields). Unknown keys are ignored.
- The key-reuse collision validator hard-error was removed (reuse is legal now).
Deferred (follow-ups): inline override of the *global* params (skip/zoom/volume/
use_audio_channels/pause_narration) — the renderer reads these from `video_source` in ~13
places, so wiring them per-event is a separate change; stripping the moved fields from
videos.json (kept as fallback defaults for now); a validator warning for unknown/unwired
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 (video0video6, …) 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.