Run-of-Show Guide - UNI Production Platform
How to read this page
A Plain and a Clear version of this page have not been written yet. What follows is the document itself.
Precise — the source document
This is the document. Rendered from the repository at the commit above, with nothing rewritten for the web. A gate re-renders it on every deploy and fails the build if a single byte differs.
Authored against docs/UNI_PRODUCTION_PLATFORM.md (the fixed master design). This guide
is a design/reference, not a deployed system. It explains the beat schema, how the
producer executes a template, how to author a new template, how language and standby
work, and walks one Interview slot beat-by-beat.
All scene names and MCP verbs here are the fixed contracts from the master design. Do not introduce new scene names or verbs - the eight scenes and the producer verb set are closed.
1. What a run-of-show is
A run-of-show is a declarative, ordered list of beats that the UNI Producer
(uni-producer) and the scheduler/playout (uni-playout) execute to drive a live show.
Nothing in a run-of-show is imperative code: it is data the producer reads, turning each
beat into a small sequence of production-MCP calls. Three nested artifacts:
templates/*.yaml -> a TEMPLATE = one named segment (a list of beats)
slot-4h.yaml -> a SLOT = a 4-hour block chaining several templates
weekly-grid.yaml -> the GRID = 7 days x 3 slots/day across time zones + languages
The eight templates: news-desk, interview, panel, explainer, colony-live,
film-playout, qa-chat, standby.
2. The beat schema (use this exactly, everywhere)
Every beat is one object with these fields:
- id: iv-03-admit-guest # stable unique id within the template
name: Admit the guest to air # human label (shows in the control UI run-of-show)
scene: GUESTS # FIXED scene set: COLONY GLASS GUESTS CLIP NEWSDESK TITLE STANDBY PIP
durationSec: 5 # nominal beat length; the playout clock can flex ambient/film beats
language: en # one of: en es fr it pt hi (drives narration voice + caption lang)
overlays: # zero or more overlay mutations (each -> a set_overlay MCP call)
- layer: lowerThird # layer = a key of broadcast.json: onAir lowerThird title ticker caption music nowPlaying
payload: { visible: true, kicker: UNI EXPERT, title: "Dr. A. Rivera", subtitle: "...", tone: ok }
narrate: { text: "...", lang: en } | null # null = no narration; object -> a narrate() MCP call (Piper, auto-duck)
music: { volume: 0.10, duck: true } # music bed state for the beat -> set_music_volume + duck
clip: { clipId: "BNB-P1-03", mode: fullframe } | null # non-null -> a roll_clip() MCP call
notes: "Producer crib - which verbs, what is human-gated, honesty fences."
Field -> MCP verb mapping
| Beat field | Producer action | MCP verb(s) |
|---|---|---|
scene |
program cut/transition to that scene | cut_to(scene, transition?, ms?) |
overlays[].layer/payload |
write each overlay layer into broadcast.json |
set_overlay(layer, payload) |
narrate (non-null) |
Piper TTS to the narration bus, music auto-ducks | narrate(text, lang, voice?) |
music.volume |
ride the music bed | set_music_volume(level) |
music.duck |
duck/unduck under speech | duck(on, target_db?) |
clip (non-null) |
roll a catalog clip into the CLIP scene | roll_clip(clipId, mode?) |
layout in notes (talking-head/panel/PIP) |
guest layout on the stage page | set_layout(template) |
guest admit in notes |
green-room -> on-air (human-gated) | admit_guest(guestId, layout?) |
segment hop in notes |
launch the next template | start_segment(template, params) |
set_overlay writes the layer into the shared broadcast.json; the transparent overlay
pages at :8099/overlays/*.html poll that file and re-render (no WebGL - 2D-canvas/CSS,
so it captures clean in OBS). The layer names are exactly the broadcast.json keys.
A template is { template, description, defaultLanguage, beats: [...] }. Templates
that take per-placement inputs also carry a params: block (referenced as {{param}}
inside beats - the slot/grid supplies real values).
3. How the producer executes a template (the beat clock)
uni-producer is the generalised director_show.cjs seam ("replace the timer with cues
from the Producer beats"). It runs a deterministic beat clock:
load template (+ params)
for each beat in order:
t0 = now
cut_to(beat.scene) # one program cut
for ov in beat.overlays: set_overlay(ov.layer, ov.payload)
set_music_volume(beat.music.volume); duck(beat.music.duck)
if beat.clip: roll_clip(beat.clip.clipId, beat.clip.mode)
if beat.narrate: narrate(beat.narrate.text, beat.narrate.lang) # auto-ducks; clock waits for TTS to finish or beat end, whichever is later
wait until (t0 + beat.durationSec) OR an operator/LLM override arrives
advance
Two layers ride on top of the deterministic clock:
- Operator + LLM (creative): the operator speaks/types ("cut to colony", "lower third for Dr. Rivera", "admit the guest"); STT/intent (or the chat LLM, the UNI-expert Claude persona) turns that into the same MCP verbs, interrupting or extending beats. The clock is the floor; the human is the ceiling.
- Auto-duck: any
narrate()(or live mic VAD) ducks the music bed automatically; the beat'smusicblock is the resting state it returns to.
Gating: the operator opens a live session once (an operator-set autoapprove
allowlist scoped to the in-show verbs - cut_to, set_music_volume, duck, narrate,
set_overlay, roll_clip, start_segment, set_layout, remove_guest). Inside the
session those run without per-call prompts but are fully audited. The outward-facing
verbs - admit_guest, schedule, start_broadcast, stop_broadcast - always need
an explicit human decision (and start/stop add a 2-step dry-run -> confirm handshake).
The producer agent never holds the operator token; it can only propose (GAP G-PA,
Class-Sec, unproven until a captured red-team run).
Playout / clock flexing: uni-playout owns wall-time position so the weekly grid
stays aligned. Ambient beats (colony-live) and film beats (film-playout) are elastic:
playout extends/loops them or trims live blocks to hit each slot's clock. Film beat
durations come from the catalog (manifest.json total_duration_s).
4. How to author a NEW template
- Copy an existing template in
templates/whose shape is closest (interview for a guest segment, explainer for graphics, film-playout for a clip, etc.). - Set
template,description,defaultLanguage. Add aparams:block only if the slot/grid must pass values in (guest ids, clip ids, language, topic). - Write
beats:in order. For each beat:- Pick a
scenefrom the fixed eight only. Never invent a scene. - Set
durationSec(nominal) andlanguage. - List
overlaysusing only the broadcast.json layer keys:onAir,lowerThird,title,ticker,caption,music,nowPlaying. Payloads must match the schema inschemas/broadcast.schema.json. - Set
narratetonullor{text, lang}. Keep narration inside the honesty fences (docs/press/05_CLAIMS_AND_FENCES.md): no AGI / no "beats LLMs"; "cache hit" not "memory"; UNI math stays private. Aspirational lines are framed as aspiration. - Set
music: { volume, duck }- ducktruewhenever a human/narration speaks. - Set
cliptonullunless the beat rolls a catalog clip. - Write
notes:naming the exact MCP verbs the beat fires and flagging anything human-gated (admit_guest / start_broadcast).
- Pick a
- Reference the new template from
slot-4h.yaml(asegments[]entry) and place it inweekly-grid.yamlif it earns a recurring slot. - Validate: scene names in the fixed set; overlay layers are real
broadcast.jsonkeys; every speaking beat ducks; no banned-unqualified words in any on-airnarratetext.
5. How language works
- A beat carries a
language(one ofen es fr it pt hi). It selects (a) the Piper narration voice for that beat'snarrate, and (b) the caption language the caption overlay shows;uni-bcast-captions(faster-whisper) can add translated tracks. - A template has a
defaultLanguage; per-beatlanguageoverrides it. - A slot has one
language(set by the grid); the slot's segments inherit it unless a template/beat overrides for a deliberate multilingual moment. - The weekly grid assigns one native language per slot per time zone, so each zone's
evening window is delivered in its own language. Across the week all six catalog
languages appear:
en(Americas + Europe + Asia lingua-franca),es(LatAm + Spain),pt(Brazil + Portugal),hi(the daily India window),fr(France),it(Italy). - Hindi (
hi) may use the ClaudeSpeak EN+HI Hinglish code-switch engine when a script mixes Devanagari + Latin; pure-Hindi scripts use ahi_INvoice. Write Hindi in Devanagari, English in Latin - the engine switches voice by script.
6. How standby works (the watchdog floor)
standby.yaml is the loop the watchdog cuts to. On any source or encoder glitch,
uni-playout immediately cut_to(STANDBY) (last-frame hold -> standby reel) and sets
onAir.text = "STANDBY" (honest - not "LIVE"). The reel beat loops (roll_clip(reel, mode=loop)) - standby never goes to black. The producer holds in standby until the
health probe reports the live source recovered, then resumes the interrupted slot at its
next live beat, with the slot clock preserving wall-time position so the grid stays
aligned. Standby is also the between-slots holding pattern in the grid. Watchdog =
systemd Restart=always + a health probe.
7. Worked example - one Interview slot, beat by beat
Placement: weekly-grid.yaml -> MON-S3, startUtc 16:00Z (Europe evening), language
en. uni-playout loads slot-4h.yaml, reaches BLOCK A - INTERVIEW, and runs
templates/interview.yaml with params { guestId: GUEST-A, guestName: "Dr. A. Rivera", guestTopic: "Trauma and the nervous system", language: en }. The operator has opened a
live session (in-show verbs auto-approved; outward-facing verbs still gated).
Beat iv-01-open (TITLE, 7s). Producer: cut_to("TITLE");
set_overlay("onAir", {value:true, text:"LIVE"});
set_overlay("title", {visible:true, kicker:"INTERVIEW", text:"A conversation", subtitle:"The science behind the mission"});
set_overlay("nowPlaying", {segment:"Interview", lang:"en", clipId:null});
set_music_volume(0.28); duck(false). No narration. The title overlay page (polling
broadcast.json) shows the card; music is up because no one is speaking. Clock waits 7s.
Beat iv-02-host-intro (NEWSDESK, 30s). cut_to("NEWSDESK");
set_overlay("title", {visible:false});
set_overlay("lowerThird", {visible:true, kicker:"HOST", title:"Welcome", subtitle:"Setting up todays conversation", tone:"ok"});
set_overlay("ticker", [{text:"Guest joins from the green room - admitted by the host", tone:"ok"}]); set_music_volume(0.12); duck(true). The host speaks live; the mic VAD
keeps the bed ducked. The guest is in the LiveKit green room (off-air); the operator
sees them on the control UI but the audience does not.
Beat iv-03-admit-guest (GUESTS, 5s) - HUMAN-GATED. The operator decides to bring the
guest up. admit_guest("GUEST-A", layout="talking-head") is outward-facing, so it
prompts a human decision (it does not auto-run inside the live session). On approve:
the guest moves green-room -> on-air room; producer set_layout("talking-head") on the
stage page; cut_to("GUESTS") (OBS captures the LiveKit stage page two-shot);
set_overlay("lowerThird", {visible:true, kicker:"UNI EXPERT", title:"Dr. A. Rivera", subtitle:"Trauma and the nervous system", tone:"ok"}).
Beat iv-04-interview-body (GUESTS, 240s). Two-shot talking-head holds.
set_overlay("caption", {visible:true, lang:"en", text:""}) - uni-bcast-captions
streams live transcript lines into the caption layer (and translations for the audience
language picker). Music stays ducked at 0.08. The host drives the conversation; the
operator can fire set_overlay("lowerThird", ...) to flip topic chips. The clock floor is
240s but the host/LLM can extend the beat to fill the block; the next beat does not start
until the operator cues it or the clock plus any override elapses.
Beat iv-05-thank-release (NEWSDESK, 20s). cut_to("NEWSDESK");
set_overlay("lowerThird", {visible:true, kicker:"HOST", title:"Thank you", subtitle: "Wrapping the conversation", tone:"ok"}); set_overlay("caption", {visible:false}). The
host thanks the guest; remove_guest("GUEST-A") (session-auth, no prompt) drops them back
to the green room. set_music_volume(0.14); duck(true) (host still speaking).
Beat iv-06-out (TITLE, 6s). cut_to("TITLE");
set_overlay("title", {visible:true, kicker:"INTERVIEW", text:"Thank you for watching", subtitle:"The feed continues"}); set_overlay("lowerThird", {visible:false});
set_music_volume(0.28); duck(false) - music lifts on the sting. The template ends;
uni-playout start_segments the next slot segment (BLOCK B - EXPLAINER).
Every one of those calls returns the metadata() provenance envelope and is appended to
the audit log. The one human gate in the whole segment was admit_guest - exactly the
outward-facing boundary the gating model requires.
Status (honest)
- This is a design/reference authored 2026-06-21 against
docs/UNI_PRODUCTION_PLATFORM.md. Nothing here is deployed; every "the producer does X" is a proposal, statuspending, not a statement that anything runs. - AMENDED 2026-07-29 — one file in this directory is NO LONGER a proposal.
first-show.rundown.jsonis bound to the live studio: every scene it names was read fromviewer/runtime/templates.jsonand checked to exist, every super was tested against the real overlay fence atviewer/command_center.cjs:313, andverify_rundown.cjsnext to it re-checks all of that on demand and is proved to fail four ways. The eighttemplates/*.yamlbeside it, and../producer/run.py, remain design/reference and remainpending— they declare a third scene vocabulary (COLONY GLASS GUESTS CLIP NEWSDESK TITLE STANDBY PIP) that matches neither the live 32 nor the retired six. Do not drive the studio from them. - No banned-unqualified word is used as a claim (avoided: verified, proven, guaranteed, isolated, secure, 100%, certified, real; used instead: checked / observed / as captured / reported / appears / pending confirmation).
- Evidence posture: the templates/slot/grid are buildable data authored to the fixed contracts; their on-air behavior is pending a containerized run on the broadcast node. Related open gaps: G-PA (producer cannot self-approve a go-live/cut - Class-Sec, unproven until a captured red-team run), G-ENC (encoder node/GPU is an operator hardware choice), G-CAP (multilingual caption latency unmeasured), G-MUSIC (no music bed asset yet - must source CC/royalty-free), G-9x16 (vertical catalog clips must be pillarboxed/shorts-walled).
- The business stack (
solutionwright-*, odoo, jitsi, cloudflared, portainer) is never a mutation target and the producer agent cannot self-approve; the encoder is not co-located with the ERP appliance. All mutating MCP verbs route through the human approval gate; outward-facing verbs always require an explicit human decision.
sha256 8db8603d53e56608 — of the original file, so what was ingested stays checkable.