audio-dashboard
Skill0 starsChanged 16 days agoApache-2.0View on GitHub
Build audio narration dashboards. Triggers: STT-after-TTS exact word-timing, real word-click-seek read-along dashboard, AfterCode workflow, Lakebed delivery.
- Pipes a download into a shell
- Installs packages
In this repository64 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- verify-lead-waitSkill
- verify-merge-receiptsSkill
- branded-docSkill
- explanatory-docSkill
- 1passwordSkill
- agada-benchSkill
- agent-browserSkill
- agent-routingSkill
- architectural-conformance-auditSkill
- brain-store-fallbackSkill
- claude-desktop-researchSkill
- cmux-agentsSkill
- cmuxSkill
- coachSkill
- coderabbitSkill
- codex-workflowsSkill
- collab-monitorSkill
- content-demo-creationSkill
- convention-auditSkill
- cursor-workflowsSkill
- cyberSkill
- deploy-verifySkill
- drive-filingSkill
- eas-prebuild-checkSkill
- ecosystem-healthSkill
- eli5Skill
- false-green-gateSkill
- figma-loopSkill
- fleet-wrap-gateSkill
- fleet-wrapSkill
- frustration-captureSkill
- gemini-researchSkill
- git-guardianSkill
- github-researchSkill
- golem-installSkill
- grill-meSkill
- heredoc-guardSkill
- html-dashboardSkill
- human-confirm-gateSkill
- i-have-adhdSkill
- install-runbook-linterSkill
- large-planSkill
- m1-window-gateSkill
- model-pin-gateSkill
- never-fabricateSkill
- orcSkill
- plan-councilSkill
- pr-loopSkill
- qa-verdict-gateSkill
- qa-videoSkill
- repogolemSkill
- research-lifecycleSkill
- reviewer-order-gateSkill
- session-handoffSkill
- skill-creatorSkill
- stitch-designSkill
- t3-visualsSkill
- tmp-blockSkill
- The first 60 shown.
The file
Show its rules and text
We split this skill into the rules it gives an agent
22 rules. About 0 are common, 8 are distinctive, and 14 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈8
Distinctive · estimate
This author's own choices. Worth a look.
14
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 10
Run /audio-dashboard creation-steps: read and follow workflows/creation-steps.md, the ordered procedure from intake to delivery.
- Distinctive: line 20
ONLY publisher. Builders never deploy a dashboard shell themselves.
- Distinctive: line 51
Do not use this for listen-only podcasts with no teleprompter requirement.
- Very specific: line 60
Use cinema only when the request explicitly asks for it.
- Distinctive: line 113
Follow Card detail affordances: text-only cards must not repeat their text in a View details expander.
- Distinctive: line 124
NOT an account provider: BUILD PASS proves static wiring, not authenticated sync.
- Distinctive: line 128
Check browser-computed text/fill colors, composing transparency and inherited backgrounds/opacity before measuring.
- Very specific: line 148
Add optional sources: [{ label, url }] (up to 12 links, labels up to 160 characters) and/or copyPrompt: { label, text, notes?
- Very specific: line 167
Never infer a repository from a bare number.
- Distinctive: line 200
Run these skill-local entries for the AfterCode read-along path (all relative to this skill dir):
- Very specific: line 217
When using voice profile names in reference, set NARRATIONLAYER_PROFILES_FILE to the machine-local profiles YAML, or set …
- Distinctive: line 222
If the acoustic or onset-energy gate flags a TTS segment, re-roll only that segment with cache disabled:
- Very specific: line 251
If the job spec includes a non-empty decisions array, this step also renders skill-local decision/answer boxes natively before writing HTML.
- Very specific: line 262
Every gate upserts a BUILD row in <output-basename>.receipts.json.
- Very specific: line 283
Only this final check can return delivery PASS.
- Very specific: line 370
Run it in a persistent session after setting the machine-local environment (for example NARRATIONLAYER_PROFILES_FILE and qwen3 daemon …
- Very specific: line 499
Gate:
scripts/render-done-gate-cli.mjs— runs before DONE: Before emitting a "render done / give it a play / here's the mp3" message, … - Very specific: line 511
Before calling a dashboard done, verify these conditions (enforced by src/audio-dashboard-evidence.mjs via scripts/validate-evidence.mjs):
- Very specific: line 542
Every initial lineage, including briefings on a fresh machine, requires the job to carry design.firstInLineage: { reason, approvedBy }: …
- Very specific: line 562
Only validate-evidence --delivery with verdict PASS advances accepted history.
- Distinctive: line 687
Absolute backstop for poisoned sibling medians: when more than half of a same-role set is incident-heavy, sibling medians can normalize the …
- Very specific: line 813
If the upstream path is missing, the lint emits a typed UPSTREAM_UNAVAILABLE record with value stamp-only and verdict DEGRADED.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Audio Dashboard
Building a dashboard?
Run /audio-dashboard creation-steps: read and follow
workflows/creation-steps.md, the ordered procedure
from intake to delivery. This is a workflow argument to the skill, not a separate
catalog command. This skill owns all build instructions; orc only relays the
filled intake block and Etan's extra instructions.
Ownership and delivery
skill-creator owns this skill, templates and BUILD gates. NarrationLayer owns the Lakebed app, relay/media server, account answer sync and UploadThing, and is the ONLY publisher. Builders never deploy a dashboard shell themselves. Audio may use CDN/relay or tailnet media fallback (8447); tailnet may host redirects, not the shell. Audio delivery follows the html-dashboard Lakebed/allowedEmails standard through NarrationLayer; see publish contract v2.
Purpose
This is the one canonical golem-powers skill for audio narration dashboards, and
it is self-contained and transferable: the whole engine is vendored under
vendor/ and driven by skill-local scripts under scripts/. Runtime/build paths
do NOT clone or reach into $NARRATIONLAYER_ROOT, $AGENT_HTML_ROOT, or
$SKILL_CREATOR_ROOT; the opt-in maintenance lint reads only the checkout named
explicitly by NARRATIONLAYER_UPSTREAM.
The only external requirements are machine-local binaries/daemon (verified by
scripts/bootstrap.mjs) and the NarrationLayer Lakebed publish handoff.
The invariant: never ship read-along timing from WPM math, line duration, fake
words, placeholder recap text, or old GolemPlaylist generators. A correct
dashboard keeps immutable raw Whisper words.raw.json for gates, separate
script-first repaired words.json for display, real transcript text, and
word-click seek to the clicked timestamp.
When To Use
Use this for:
- AfterCode read-along dashboards.
- Narration dashboards that need exact word highlights or word-click seeking.
- Regenerating a broken audio dashboard that drifted, restarted, or showed placeholder/meta transcript text.
- Delivering an audio dashboard on Lakebed through NarrationLayer.
Do not use this for listen-only podcasts with no teleprompter requirement.
Dashboard Types
The skill exposes two separate types through the same timing, evidence, and Lakebed delivery pipeline. They share audio primitives, not presentation templates.
Type: cinema
Use cinema only when the request explicitly asks for it. Set "type": "cinema".
It stays on the vendored V4 renderer at
vendor/agent-html/templates/v4-story-mode/: a global Play-All story surface,
Q/A scenes, read-along teleprompter, and optional decision boxes appended below.
Type: decision-flow — audio decision dashboard
Decision-flow is the default for decision batches and briefings carrying decisions
or answers, including eod, huddle and commute. Explicitly set spec.type; never let
an omitted type select the legacy renderer default. Its user-facing
name is audio decision dashboard. Set
"type": "decision-flow". Its standalone template lives in
templates/decision-flow/ and does not render or overwrite cinema. Each
decision is one self-contained ledger card whose sceneIds own all of that
card's audio. The card-local play button runs those clips in order, keeps the
real-word-timed teleprompter open while playing or paused, and collapses it only
when that decision's full section completes. Word clicks seek within the section
and retain the normal clip-to-clip flow. Next and Skip acknowledge the action,
advance to the following card, and autoplay it. Picks and free text save through
the account answer store. Without a bridge, the visible device-only fallback uses
dbx:<spec.id> (or the spec's custom storage key). Copy answers exports paste-ready
markdown. Playback rate and list/story preferences stay on this device.
Each card also exposes ↻ Restart, which restarts that decision's audio and
teleprompter section from the beginning without advancing to another card.
Decision-flow is parallel to cinema, not a replacement. Cinema remains the
pure-listening narrated-digest/story surface with global Play-All. A decision
dashboard may set cinemaUrl to expose a small Cinema listening mode link to
its parallel rendition.
Author decision-flow narration for the operator, not for the agents who coined the vocabulary: assume the listener has zero prior context. On first use, define every internal term, codename, and agent-coined label in plain language inside the narration itself, including what happened, why it matters, and what changes between the options. Never ask the operator to decide on unexplained jargon or rely on a separate plan, collab log, dashboard card, or glossary to make the audio understandable.
Decision-flow fails closed unless every scene is owned by exactly one decision. This keeps audio and questions woven together and prevents a cinema-style global audio strip / separate question list from reappearing.
Card detail format
BUILD rejects CARD_TEXT_AUDIO_MISMATCH when more than 10% of rendered card
prose or owned scenes' authored script words lack an ordered match in the other
(longest common subsequence, normalized case, punctuation and whitespace).
It compares summary plus body; titles, rail metadata, controls, options,
Sources/copyPrompt and code blocks are excluded. The one threshold lives in
src/card-text-audio-gate.mjs. An honest summary declares audioSummary: true
or a nonblank summary; the renderer visibly labels it Audio is a summary of
this card, and BUILD requires that label outside the collapsed answer sheet.
Follow Card detail affordances: text-only cards must not repeat their text in a View details expander. Either offer no secondary action, or offer a Listen deeper button that opens an inline narrated section with real-word-timed read-along and word-click seek. Cards with a visualization may keep a View details expander that shows the visualization's underlying data.
Answers save to the account through window.__answerStore (or the parent bridge),
using a stable dashboard-id meta. localStorage is only a visible device-only
fallback (the local keys described in this file are fallback keys); never ship
device-only. Answer inputs require an explicit, stable spec.id.
BUILD rejects missing identity or answer-store wiring. Embedded consumer code is
NOT an account provider: BUILD PASS proves static wiring, not authenticated sync.
Badge contrast
Every badge/pill label (including recommended and REBOOT) must have WCAG AA text contrast of at least 4.5:1 against its effective fill, at every size and supported state/theme. Use .ad-badge in either template for dark text on a light fill; white text requires a sufficiently deep fill. Never use pale text on pale green or a 3:1 large-text exception for badges.
Check browser-computed text/fill colors, composing transparency and inherited backgrounds/opacity before measuring; custom colors and states must also pass without rounding a value below 4.5 into a pass. This rule covers badge text, not all WCAG requirements.
Copyable instructions
Attribution: Etan, 2026-11-03.
Copy blocks are only for what the reader runs or pastes: terminal commands,
CLI words, literal text to paste, and file paths to paste. Render these as code
with a one-click Copy button and visible Copied confirmation. In either
type, use a triple-backtick fence in decision.body; keep a multi-line command
sequence in ONE fence, copied in order. Keyboard activation, clipboard fallback
and resetting feedback are shared.
URLs stay normal clickable links; never require or suggest a copy block for a
link. Bare HTTP(S) URLs and [label](https://example.com) in card bodies render
as links. A URL inside a shell command belongs in that command's copy block.
BUILD rejects unprotected commands and pasteable paths on cards and the
decision-flow header, records COPY_BLOCK_REQUIRED in <output>.receipts.json,
and withholds HTML. Ordinary prose and links pass. There is no bypass.
Per-card Sources and research prompts (decision-flow)
Add optional sources: [{ label, url }] (up to 12 links, labels up to 160 characters)
and/or copyPrompt: { label, text, notes? } to a decision. Sources uses one native
disclosure, closed by default and keyboard accessible; links open a new tab with
noopener/noreferrer. URLs must be HTTPS or relative inside the dashboard directory;
schemes such as javascript/data/http, protocol-relative URLs and directory escapes reject.
copyPrompt.label names the button (up to 80 characters); text is copied exactly,
including whitespace. notes is visible above the button (up to 280 characters):
state whether Google Docs must be enabled in Gemini and whether research lifecycle
has run. Successful copies show Copied; unavailable clipboard access offers a
selection-copy fallback and, if that fails, the selectable prompt with a failure message.
These fields are reference material, never narration/teleprompter text or saved answers.
Issue and PR references
Attribution: Etan, 2026-10-28 dashboard feedback (AD-16).
Any #N a card shows must be a link. Add a rail row on that card whose href
is the repository-qualified PR/issue URL the producer already has:
{ "label": "PR", "value": "#39", "href": "https://github.com/OWNER/REPO/pull/39" }.
Never infer a repository from a bare number. The build fails on an unlinked
#N, on a #N row whose own href is not that PR/issue (a generic URL does not
count), and on a value whose number contradicts its GitHub href. GitHub links
render as repo#N. A #N inside a code span or right after a colour word
(color #123, background: #123456, fill=#333) is a CSS colour, not a
reference; everywhere else it is one. Only http(s) hrefs become anchors; anything else,
including javascript: and data:, stays plain text.
Vendored Engine (what makes it portable)
Everything the pipeline needs is inside the skill:
| Concern | Skill-local file |
TTS runner (fail-closed voice-profile gate, qwen3 daemon, splitForBreathing) | vendor/narrationlayer/local-tts-runner.ts (+ profiles.ts, text-normalize.ts) |
| Spoken-form/pronunciation overlay (synth input only) | vendor/narrationlayer/pronunciation.yaml + pronunciation-config.ts |
| STT-after-TTS word timing | vendor/narrationlayer/word-timings.ts (runWhisperCliWordTimings) |
| DP alignment / repair to script tokens | vendor/narrationlayer/word-timing-repair.ts (normalizeWordTimingsForScript) |
Cinema renderer (Q/A, Play-All, teleprompter, .note-area) | vendor/agent-html/lib/render-v4.mjs + vendor/agent-html/templates/v4-story-mode/ |
| Decision-flow renderer (ledger cards, card-local audio, answers) | src/decision-flow.mjs + templates/decision-flow/ |
| Cinema QA | vendor/qa/verify-cinema.mjs |
| Decision-flow QA | vendor/qa/verify-decision-flow.mjs |
| Evidence gate | src/audio-dashboard-evidence.mjs |
| Acoustic artifact + onset-energy BUILD gates | src/acoustic-artifact-gate.mjs |
| Teleprompter drift BUILD gate | src/teleprompter-drift-gate.mjs |
| Transcript-fidelity BUILD gate | src/transcript-fidelity-gate.mjs |
| BUILD receipts sidecar | src/build-receipts.mjs |
| Rejected-take cache purge | src/take-cache.mjs |
| Narration vendor provenance + two-way drift lint | vendor/narrationlayer/VENDOR-VERSION + scripts/lint-narration-vendor-drift.mjs |
| Orchestration | scripts/synth-segments.mjs, scripts/build-dashboard.mjs, scripts/bootstrap.mjs, scripts/validate-evidence.mjs, scripts/audio-dashboard-generator.mjs |
Canonical Pipeline
Run these skill-local entries for the AfterCode read-along path (all relative to this skill dir):
- Bootstrap —
bun scripts/bootstrap.mjsverifies bun, ffmpeg/ffprobe, whisper-cli + model, and the qwen3 daemon. It FAILS LOUD (never silent) if a hard dependency is missing. Synthesize + time —
bun scripts/synth-segments.mjs --spec <job.json>. For each scene it runs the vendoredlocal-tts-runner.ts(splitForBreathingcadence, fail-closed voice-profile gate) to produce the WAV, transcodes an mp3, runsrunWhisperCliWordTimingson the WAV, then DP-aligns the audio words to the script tokens withnormalizeWordTimingsForScript. This is the exact STT-after-TTS step. It writes immutable raw Whisperwords.raw.jsonplus a separate repaired displaywords.json({index,word,start,end}) per segment and refuses to ship estimated/even-split timing (exits nonzero if alignment matches zero script words, or whisper returns nothing). BYO-audio: a scene may setaudioWavto an existing real speech WAV to skip TTS (no daemon needed) and still get real STT timing. When using voice profile names inreference, setNARRATIONLAYER_PROFILES_FILEto the machine-local profiles YAML, or setNARRATIONLAYER_ROOTso the vendored runner can find profiles. Long synth/render runs must run inside a persistent session or detached daemon, not a backgrounded shell job that dies with the terminal. If the acoustic or onset-energy gate flags a TTS segment, re-roll only that segment with cache disabled:bun scripts/synth-segments.mjs --spec <job.json> --resynth-scene <segment-id> --no-cacheThis is mandatory for flagged TTS re-synths: cache-hit re-synth returns the identical glitched take, which is a silent false-green. For a flagged BYO segment, replace or edit its
audioWavsource instead;--no-cachecannot repair operator-supplied audio. BUILD attempts the frozen-take purge when acoustic-artifact, onset-energy, or transcript-fidelity rejects TTS audio and emits the authoritativePURGED,MISSING,SKIP, orERRORoutcome; the same boundary recordsSKIPfor BYO audio even if a stale TTS receipt exists.--resynth-sceneimplies--no-cache; keep the explicit flag in TTS runbooks so the intent is visible. The TTS runner clears any old WAV-to-key receipt before an attempt and commits a new receipt atomically only after the selected take validates, so a failed reroll cannot rebind stale audio to a new cache key.- Render —
bun scripts/build-dashboard.mjs --spec <job.json>routes byspec.type. The explicitly requestedcinemapath renders the vendoredrender-v4.mjscinema-with-answers;decision-flowrenders the separate skill-local ledger template. Both use real per-word timing (realWordTiming: true), embedding each mp3 as a data URI, and writes the durable HTML tospec.outputPath(a repodocs.local/dashboards/*.htmlsource). It refuses to write into adashboards-servetree. It also fails closed if rendered timing no longer exactly preserveswords.jsonword spans, if word spans overlap/go backward, or ifabs(tpdata.total - ffprobe(mp3).duration) > 0.5s. If the job spec includes a non-emptydecisionsarray, this step also renders skill-local decision/answer boxes natively before writing HTML. The native surface saves radio picks and free text through the account answer store;dbx:<spec.id>is only its default visible localStorage fallback key. It also includes a Copy-answers markdown export. Optional answer harvest is exposed viaspec.answerSinkorspec.decisionSurface.answerSink. BUILD runs four fail-closed gates before writing HTML: transcript fidelity compares RAW Whisper words to the script, the Acoustic Artifact Gate checks duration and pitch invariants, onset energy checks the opening WAV window, and teleprompter drift compares RAW Whisper words to the final renderedtpdata. A bad take cannot be hidden by repaired display words. Every gate upserts a BUILD row in<output-basename>.receipts.json; the schema-v1 sidecar includes the HTML SHA-256, portable engine disclosure, thresholds used, typed violations, and completed cache purges. It contains no absolute paths or host identity and survives a REJECT even though the HTML is withheld. - QA — run the verifier for the selected type:
bun vendor/qa/verify-cinema.mjs <html>orbun vendor/qa/verify-decision-flow.mjs <html>. Both report scene/word coverage and monotonicity; decision-flow additionally verifies exact scene ownership, card-local controls, answer persistence, and the absence of a cinema/global Play-All surface. - Prepare handoff —
bun scripts/validate-evidence.mjs --spec <job.json> --prepare-publishruns pre-publish evidence plus QA and writes<output-basename>.publish-handoff.jsonand its media manifest. The outcome is READY_FOR_HOSTED_PUBLISH, never delivery PASS. The handoff carries the source directory, persisted answerKeys and inline mediaManifest; see the contract. - Hosted delivery (NarrationLayer only) — hand off those files, the built
HTML, BUILD receipts, job and media. NarrationLayer stages the real account
provider, proves a disposable note round trip, promotes that same candidate,
and returns
<output-basename>.delivery.json. Runbun scripts/validate-evidence.mjs --spec <job.json> --delivery <receipt.json>. Only this final check can return delivery PASS. Tailnet sync strings and HTTP 200/byte probes cannot substitute for authenticated account proof. Deprecated runtimes return PASS_DEPRECATED_RUNTIME (exit 5); rebuild with the current runtime. The publisher owns its runtime sha allowlist. Historical pages published outside admission may receive BACKFILL_ACCOUNT_PROVEN (exit 4): an honest account-proof record, never acceptance or PASS. Backfill requires the contract's three markers and full account proof; mixed claims reject.
The default interaction shape is decision-flow with card-local audio, real-word
read-along and .note-area response textareas with
account persistence and a visible device-only fallback when no bridge is available,
so Wispr/VoiceBar can dictate into the focused input.
Spoken-Form and Pronunciation Config
The vendored engine loads vendor/narrationlayer/pronunciation.yaml before each
TTS request. It collapses same-line spaced capitals (P R → PR) and then
applies the map, so ecosystem expansions are config rather than authoring hacks:
acronyms:
PR: "pull request"
QA: "quality assurance"
heteronyms:
live streams: "lyve streams"The format matches ~/.voicelayer/pronunciation.yaml: a top-level category with
indented term: "pronunciation" entries. To add or replace per-install terms,
set NARRATIONLAYER_PRONUNCIATION_FILE to one or more path-delimited YAML files.
The shipped file loads first and later files override earlier entries
case-insensitively. A configured file that is missing, malformed, or empty fails
synthesis loudly.
Expansions and respells ride the ENGINE channel only. The renderer, repaired
words.json display series, and every teleprompter continue to receive the
original scene script. This is the B11 two-channel law: pull request or lyve
may enter the daemon input and .spoken.txt, but P R or live remains on
screen exactly as authored.
The skill-local src/spoken-form.mjs prepares clock times and a small abbreviation
set before synthesis: 9pm / 9 p.m. → nine p m, at 21:00 →
at nine p m, 9:05pm →
nine oh five p m, and 11:30am → eleven thirty a m. Lowercase spaced letters
avoid the dotted-identifier rule. Bare HH:MM reads as plain numbers unless
preceded by at/by/until/from; only clock contexts infer am/pm. PR #123,
PR 123, and pull request #123 say pull request one twenty-three.
Other #N references say number: issue #12 becomes issue number twelve.
e.g., i.e., vs., approx., ~5, 5%, 2x, and v2.3 expand to ordinary
speech; 2.5x says two point five times. Prefix forms such as x2 and x86
stay unchanged. Ambiguous compound clock, PR, range and multiplier tokens stay
literal rather than expanding a prefix or suffix. Explicit am/pm ranges expand
both ends; ~98.5% says about ninety-eight point five percent. Only lowercase vs expands. Timing aliases use comparison-form deduplication and keep
independent changed terms separate, without single-digit partial aliases.
Complete backtick code spans/fences, tilde fences
and HTTP(S) URLs stay verbatim through both normalization layers; standalone PM
names stay unchanged. scripts/local-tts.mjs supplies this preparation to the
vendored runner; BUILD recomputes the same bytes before trusting .spoken.txt.
These deterministic forms have offline timing/gate coverage; qwen3 voice quality
is verified at the next real build, not inferred from unit tests.
The TTS runner persists .spoken.txt as required provenance containing the exact
synth-input bytes. STT repair derives only the lexical spans that differ between
that transcript and the authored display script, then treats those spans as
approved aliases when attaching real Whisper timings to display tokens. The raw
Whisper series remains untouched; transcript fidelity checks the synth transcript,
and teleprompter drift accepts only the derived aliases. A missing or empty synth
transcript fails the TTS pipeline rather than falling back to estimated timing.
Context-dependent heteronyms currently use exact context phrases, longest first.
The shipped live examples cover the real s9q broadcast phrases while leaving
the /lɪv/ sense (“I live in Riverton”) untouched. Add a new phrase explicitly for
a new context; this is intentionally narrower than linguistic disambiguation.
The engine does not automatically read ~/.voicelayer/pronunciation.yaml:
full shared-file wiring is the spec-gated B8 unification and remains out of scope
until that contract is ratified. An install may explicitly point at a compatible
overlay in the meantime.
AfterCode Workflow
Dry-run the exact skill-local plan:
bun scripts/audio-dashboard-generator.mjs --workflow aftercode --dry-runRun it in a persistent session after setting the machine-local environment (for
example NARRATIONLAYER_PROFILES_FILE and qwen3 daemon token), and only when bun, whisper CLI,
the qwen3 daemon, and the
job spec are ready:
bun scripts/audio-dashboard-generator.mjs --workflow aftercode --run --spec <job.json> --job-dir <job directory>The wrapper stays thin. It delegates to the vendored engine + skill-local orchestration scripts, never to private engine checkouts.
Job spec
For decision-flow dashboards, heading is a title of at most 8 words. Put status and context in subtitle, which the decision-flow template renders collapsed under Details.
The job must carry the latest accepted spec/design receipt as its baseline:
acceptedBaseline: { path: "<receipt path>", sha256: "<receipt sha256>" }.
The path is relative to the job file (or absolute). Read and verify that receipt before building; select its accepted type and design.
This source receipt complements the existing design-preservation predecessor.
--spec points at a JSON job (see examples/job.json):
{
"id": "my-dashboard", "type": "decision-flow",
"title": "…", "kicker": "…", "heading": "…", "subtitle": "…",
"outputPath": "$REPO_ROOT/docs.local/dashboards/my-dashboard.html",
"scenes": [
{ "id": "c1q", "domain": "comms-layer", "title": "Q1 · …",
"reference": "host-voice", "role": "host", "script": "…" },
{ "id": "c1a", "domain": "overview", "title": "…",
"reference": "expert-voice", "role": "expert", "script": "…" }
],
"decisions": [
{ "id": "ship-path", "title": "Ship path", "deadline": "today",
"body": "Choose the path forward.", "options": ["Option A", "Option B"] }
]
}reference is a registered voice profile name OR a direct .wav reference clip
path. A scene may also carry audioWav (existing WAV) for a daemon-free BYO run.
decisions is optional; when present it must be a non-empty array of
{id,title,body,options,deadline?} records, with options allowed to be empty
for free-text-only decisions.
For decision-flow, decisions is required and must be a non-empty array. Add
the type plus explicit scene ownership: every scene must appear in exactly one
decision's non-empty sceneIds array.
Attach a dependent decision with after: "<explainer-card-id>" in decisions.
Unattached cards retain authored order; attached decisions follow their explainer
immediately, in their authored relative order, across cards, chips and index.
BUILD refuses unknown/cyclic references or separated final attachments with
DECISION_SEPARATED_FROM_EXPLAINER. An exact explicit topic match to an earlier
status: "INFO" card requires after (DECISION_AFTER_REQUIRED); legacy cards
without topics retain their order. Titles are never guessed. IDs and persisted
answer keys, kinds and options stay unchanged.
{
"type": "decision-flow",
"id": "six-decisions",
"title": "Six decisions",
"outputPath": "$REPO_ROOT/docs.local/dashboards/six-decisions.html",
"cinemaUrl": "./six-decisions-audio.html",
"scenes": [
{ "id": "d1q", "title": "Question", "script": "…" },
{ "id": "d1a", "title": "Trade-off", "script": "…" }
],
"decisions": [
{ "id": "d1", "rank": 1, "title": "Decision one", "status": "OPEN",
"body": "Choose the path.", "options": ["Option A", "Option B"],
"sceneIds": ["d1q", "d1a"],
"rail": [{ "label": "Owner", "value": "Operator" }],
"comparison": { "url": "visuals/d1.html", "label": "View comparison", "timeoutMs": 10000 } }
]
}Comparisons settle. comparison.url is a relative page inside the dashboard
directory. The renderer and the click handler both check the URL the browser
will load, so a scheme, ../, %2e%2e/, control characters or another origin
are rejected. It opens in a shared dialog that shows Loading, then the page, or
a failure message with Retry and Back after a frame error or after
timeoutMs (default 10000; per card, or the spec-level comparisonTimeoutMs).
A load counts only with positive evidence from a readable same-origin frame: a
2xx/3xx navigation responseStatus, or, where the browser has no such field
(Safari/WebKit), the page's ready marker. Every comparison page must carry the
canonical marker: the page starts with an optional doctype, <html …>,
<head …>, then (HTML whitespace only; one leading BOM allowed) exactly
<meta name="adash-comparison" content="ready">, the first child of <head>.
data-adash-comparison="ready" on that leading <html> start tag also works.
Error pages never carry it. The build accepts only this form and fails otherwise,
even for pages a browser would accept: run hand-built or generated pages through
markComparisonReady(html) from src/decision-flow.mjs, which inserts it
without removing anything.
Serve the dashboard over http(s): from file: the frame cannot be checked, so
the dialog shows the failure state. Back and Esc return focus to the opener.
Bootstrap
Three dependency classes cannot travel inside a skill and must exist on the host.
scripts/bootstrap.mjs verifies them and FAILS LOUD with install hints — it never
silently falls back to estimated timing or system TTS.
HARD (required for any real dashboard — STT + render):
bun— runs the vendored.tsengine (curl -fsSL https://bun.sh/install | bash)ffmpeg+ffprobe— transcode / duration (brew install ffmpeg)whisper-cli— STT word timings (brew install whisper-cpp)- a whisper model — e.g.
~/.cache/whisper/ggml-large-v3-turbo.bin(or setNARRATIONLAYER_WHISPER_MODEL)
TTS-only (required only to synthesize NEW audio; BYO-audio needs none):
- a reachable qwen3 TTS daemon (default
http://127.0.0.1:8880) - the daemon auth token (
~/.voicelayer/daemon.secret) - voice profiles — set
NARRATIONLAYER_PROFILES_FILE(aprofiles.local.yaml) orNARRATIONLAYER_ROOT, OR pass a direct.wavreference(no profiles needed). Voice profiles + their reference clones are inherently machine-local and are NOT vendored.
bun scripts/bootstrap.mjs # exit nonzero if any HARD dep missing
bun scripts/bootstrap.mjs --require-tts # also gate the daemon/tokenAcceptance Gate
Gate: scripts/render-done-gate-cli.mjs — runs before DONE. Before emitting a
"render done / give it a play / here's the mp3" message, replay the turn through
bun skills/golem-powers/audio-dashboard/scripts/render-done-gate-cli.mjs <transcript|->
(exit 3 = FLAG). It requires the SAME-TURN composite probe — ls + ffprobe
(size>0, duration>0) of the claimed mp3, plus a registered cloned voice
(fail-closed on a missing profile or a silent system-TTS fallback). The artifact
must be where it is claimed: a file-on-disk at the claimed path is enough, and an
HTTP-200 or an embedded-clickable player is required only when the claim points the
listener at a served surface (dashboard / URL). A FLAG means the claim is not
yet earned: run the missing probe, then claim. Full rule table and RED/GREEN
provenance: references/render-done-gate.md.
Before calling a dashboard done, verify these conditions (enforced by
src/audio-dashboard-evidence.mjs via scripts/validate-evidence.mjs):
- A cinema deliverable uses
render-v4.mjsthroughbuild-dashboard.mjs, with Q/A sections, Play-All, read-along teleprompter, and.note-arearesponse textareas. A decision-flow deliverable usessrc/decision-flow.mjsplustemplates/decision-flow/, with no global Play-All strip and exactly one owning card per scene. Missing answer-writing areas is a hard rejection for either type. Do not generate multiple replacement designs unless the operator explicitly asks for design exploration. - The HTML includes per-section response capture:
.note-areatextareas,data-note/data-titlemetadata, account save/restore with a visible device-only fallback, and copy/export. words.raw.jsonandwords.jsonexist per segment. Every entry has a non-empty stringwordplus finite numericstartandend; raw words feed gates while repaired words feed display, and both timing series are monotonic.- The dashboard declares real word timing (
realWordTiming: true). - Clicking a word seeks audio to that word's timestamp (must not restart the segment or reload the audio).
- The visible transcript is the real script/transcript, not placeholder text.
- The durable output path is a repo
docs.local/dashboards/*.htmlfile. - Delivery requires the NarrationLayer Lakebed receipt, approved local origin, matching id/source hash and authenticated note round-trip proof.
Preserve the published design
Preserve pinned controls, labelled read-along, transcript at playback end, native
answers, code-only copy, focus, seeking and resets. Gate both outer and reading-column
width/centering by role and parent geometry. Set spec.design.lineage for a
separate product; nightly/eod/commute/huddle default to the shared briefings
lineage. Changing spec.id or spec.series never resets accepted design.
History is per-machine under ~/.local/share/audio-dashboard/design-history/lineages/<lineage>/.
Every initial lineage, including briefings on a fresh machine, requires the job
to carry design.firstInLineage: { reason, approvedBy }: nonblank one-line reason
and a named approver (never UNRECORDED). Missing/invalid approval rejects with
FIRST_IN_LINEAGE_UNAPPROVED. Per-invocation --first-build <reason>
alone is insufficient; it must accompany that approval. The handoff carries
design.firstInLineage for NarrationLayer to validate. Even with approval,
an accepted registry entry or previousPublished refuses DESIGN_LINEAGE_RESET_REFUSED.
On another machine or after registry loss, copy the accepted served predecessor
and set designPreservation: { previousPublished: "served-copy.html" }; absence
rejects without an approved initial-lineage declaration. Old series-keyed history
must be migrated the same way. Without a lineage record, previousPublished wins
over legacy series history. With an accepted lineage head, that head is used; a
conflicting supplied previousPublished rejects DESIGN_PREDECESSOR_CONFLICT.
NarrationLayer owns the authoritative deliveries.json;
the publisher must independently refuse FIRST_IN_LINEAGE for an already delivered lineage.
A waiver needs redesign: { reason, approvedBy, invariants: ["LAYOUT"], from: "previous sha256" };
blank or UNRECORDED approval rejects. Only fired codes may be listed, never on a
baseline. The waiver expires with its predecessor.
Run bun scripts/check-design-preservation.mjs --spec job.json --screenshots proof/.
BUILD/evidence compare design; handoffs carry lineage, admission and predecessorSha256.
Only validate-evidence --delivery with verdict PASS advances accepted history;
backfill, deprecated runtime, BUILD and preparation never do. An explicit owner
acceptance is also available with check-design-preservation --accept <approvedBy>
after its comparison passes. scripts/verify-tailnet-publish.mjs is a media/redirect
probe only, never delivery proof, never advances design history.
Transcript Fidelity and Teleprompter Drift BUILD Gates
src/transcript-fidelity-gate.mjs compares the raw Whisper transcript with the
script before HTML emission. It rejects strict tail prefixes or missing script tails as
TAIL_TRUNCATION, and rejects materially different, length-preserving long-word
substitutions as PHONEME_CRITICAL_SUBSTITUTION; adjacent material errors are
aggregated without losing either piece of evidence.
The substitution rule is calibrated on 33 fresh real WAV decodes: all 31 clean
scenes pass while the s9q overnight→Overtime, and s13a
websocket→WebSeaCut incidents reject. Regenerate the portable receipt with
bun evals/generate-transcript-fidelity-calibration.mjs --job <job.json> --out
evals/fixtures/calibration/2026-07-17-fable-blind-weave-transcript-fidelity.json.
Confidence is evidence, not a rejection threshold. A BYO scene without script
stays fail-closed as SCRIPTLESS_SCENE_UNSUPPORTED with a runbook to add the
script; a NOT_APPLICABLE verdict class remains a specification question.
Other transcript violations use the TTS fresh-take runbook only for TTS scenes;
a BYO violation instead requires replacing or editing its audioWav source.
Failures carry segment, source kind, metric, value, threshold, evidence, and an
actionable runbook.
src/teleprompter-drift-gate.mjs is also a BUILD publish blocker. It compares
the final rendered teleprompter words to words.raw.json across the whole
transcript, aligns contraction/split/merge lexical blocks before comparing
timing boundaries, coalesces canonical expansions after lexical fallback, and
includes tolerated substitute steps in timing comparison so a changed tail word
cannot bypass drift measurement. It weights the final third so accumulated tail
drift fires.
The banked GREEN fixture from
GREEN-FIXTURE-insync-2026-07-06 is copied into
evals/fixtures/green/03-teleprompter-drift-insync.json; the paired RED fixture
skews only the final third so a head-only check cannot false-green it. The gate
also has a latency eval and must stay under the <5s stop-class budget.
Typed failures use the plain-language TELEPROMPTER_DRIFT name and a positive
runbook: regenerate the segment with real STT word timings, rebuild, then rerun
the drift gate.
Voice Roles — Ben HOSTS, the stream author EXPLAINS
Two voices are the default for every briefing: eod, commute, huddle and nightly.
The host (ben-c1) asks a short question; the expert (<expert>-c4s, with the
person from GOLEMS_EXPERT_VOICE) explains in a separate segment. Never put
role announcements in spoken text: no "Ben asks:", "Expert:", or "Q:" / "A:".
BUILD rejects single-voice or expert-free declared-voice builds with
SINGLE_VOICE_BUILD, spoken announcements with ROLE_LABEL_SPOKEN, and rendered
role labels on cards without both roles with ROLE_LABEL_MISMATCH.
Decision-flow shows speaker role labels only when that card has both roles.
BYO scenes with no declared voice retain their existing unverifiable treatment.
A deliberate single-narrator listen-only piece may set job-level
voiceRoles: { "single": { "reason": "deliberate single-narrator listening piece" } }.
The reason must be a nonblank string; this exception is stated in the job, never
inferred. It excuses only the single-voice check, never inversion or role labels.
Public voice-role contract: Ben is the HOST. The stream author is the EXPERT. Ben asks, the stream author explains unless a job explicitly overrides the roles.
src/voice-role-gate.mjs owns this as two constants — HOST_VOICE = "ben",
EXPERT_VOICE — and nothing else in the build path may re-state it. The repo
never names the stream author: EXPERT_VOICE is read from GOLEMS_EXPERT_VOICE
(one <person> segment) and falls back to the neutral examplechannel when unset.
They are PERSONS, not profile ids: a person owns many profiles over time
(<expert>-c4, <expert>-c4s, <expert>-n4a), so pinning an id would rot on the next
cadence experiment while the ruling would not have changed. The render-done gate
and /false-green-gate derive their registered expert clones from the same variable.
A scene is gated when it declares BOTH a role of host/expert AND a voice
(reference or profile). The person is read from the profile prefix
(ben-c1 -> ben). A contradiction REJECTS the build — it does not warn —
and it rejects on the SPEC, before synthesis, because an inversion is knowable
from the job file alone.
Why this gate is not redundant with the others: every other gate measures the WAV
or the transcript, and both are perfectly consistent with the wrong voice saying
the right words. --role reaches the TTS runner as informational only
(local-tts-runner.ts:151, "No engine effect"), so before this gate role was
decorative and the two fields could disagree forever. They did, twice, in
published dashboards.
Override — stated, never inferred. For the ~10%, set voiceRoleOverride on the
scene, or voiceRoleOverrides: { "<sceneId>": "<reason>" } on the job. The value
must be a non-empty reason, so the receipt records why the ruling was set aside,
not merely that it was. A blank string is not an override. An override on one
scene never excuses another.
BUILD Receipts Sidecar
build-dashboard.mjs writes one schema-v1 sidecar next to the intended HTML:
foo.html pairs with foo.receipts.json. The writer owns the BUILD rows for
voice-role, transcript-fidelity, acoustic, onset-energy, and
teleprompter-drift, plus copy-blocks, keyed by gate plus stage. voice-role is written first
because it runs pre-synthesis. onset-energy is the
fourth additive BUILD row; bridge admission's initial three-row expected set remains
compatible until its per-install config opts into the new row. PASS HTML is
bound by SHA-256. REJECT rows are written before
nonzero exit and retain the rendered-candidate hash while the rejected HTML is
withheld. D6d purges store only cache key, segment, reason, and ISO timestamp.
SYNTH, EVIDENCE, PUBLISH-SYNC, and BRIDGE-ADMISSION append their own rows when
those stage writers land; absence is never silently treated as PASS.
Acoustic Artifact Gate
build-dashboard.mjs fails closed before HTML emission when synthesized audio violates
either sibling-relative acoustic invariant or an additive absolute backstop:
- Duration/word sibling ratio: for each segment, WAV duration divided by
words.jsonword count must be no more than1.25xthe median of same-role sibling segments. - Voiced-frame pitch outliers: 40ms frames / 20ms hop, autocorrelation f0
search 70-1000Hz, gated to loud and periodic speech frames only
(
RMS >= 0.15 x segment p90 RMSand autocorrelation peak>= 0.55). The gate counts voiced frames whose f0 exceeds2xthe segment median f0.
Calibration from the night-wrap seed: real glitch segments landed around
116-123 high-f0 voiced frames, clean siblings around 61-69, and the s3a-like
stable true-negative around 103-108. The threshold is therefore
>112 high-f0 voiced frames plus a > siblingMedian + 6 margin. Do not replace
this with global ffmpeg astats peak/RMS/flat-factor checks; those were
empirically uniform on the real glitches.
- Absolute backstop for poisoned sibling medians: when more than half of a
same-role set is incident-heavy, sibling medians can normalize the artifact.
The gate therefore also rejects
DURATION_WORD_ABSOLUTE_BACKSTOPwhen duration/word exceeds 1.25s, andHIGH_F0_VOICED_FRAME_ABSOLUTE_BACKSTOPwhen the calibrated high-f0 count exceeds 112 but the sibling median is also high.
Typed failures name the segment, role, metric, measured value, threshold, and a
source-aware recovery step. A TTS rejection routes that segment's content-hash
receipt through the purge boundary and prints the authoritative CACHE_PURGE
outcome; only a PURGED result is recorded as a completed purge in the receipts
sidecar:
bun scripts/synth-segments.mjs --spec <job.json> --resynth-scene <segment-id> --no-cache
bun scripts/build-dashboard.mjs --spec <job.json>A BYO rejection instead prints a CACHE_PURGE ... status=SKIP audit receipt and
requires replacing or editing the scene's audioWav source before synthesis and
BUILD; it never deletes a frozen TTS take through a stale receipt.
Onset Energy BUILD Gate
The onset-energy member of the acoustic family uses the same PCM-s16 WAV parser,
applies a non-cancelling downmix by retaining the highest-magnitude channel in
each interleaved frame, and emits its own onset-energy:BUILD receipt row. Its
install calibration lives in one place in src/acoustic-artifact-gate.mjs:
ONSET_WINDOW_SECONDS=0.750: RMS is measured from the exact segment boundary; leading silence counts because it makes the opener missable too.ONSET_MIN_RMS_DBFS=-35: a lower opening RMS rejects asONSET_ENERGY_ABSOLUTE_RMS_DBFS.ONSET_MAX_PEAK_DELTA_DB=26: an opening more than 26 dB below the complete segment peak rejects asONSET_ENERGY_PEAK_DELTA_DB.
The real RED fixture is an unnormalized 1221–1225 second extract from the
2026-07-16 L1 voice_ask QA recording. That surface is disclosed as the source;
the fixture transfers only the shared missable-opener defect class into the
narration BUILD gate. Its first 750 ms measure -56.547 dBFS, the extract peak is
-23.452 dBFS, and the delta is 33.094 dB. The earlier QA report's -8.13 dBFS
value is a real one-second transient on the raw source around t=1206s, about 15
seconds before this opener. It is outside the fixture window, so QA window
misalignment—not loudness normalization—explains the apparent discrepancy; the
committed fixture records both measured windows as raw-source truth.
The 33-scene fable-blind-weave-2026-07-15 calibration has zero false trips:
onset RMS spans -26.032 to -18.576 dBFS and peak delta spans 14.410 to 22.794 dB.
Regenerate the portable receipt with:
bun evals/generate-onset-energy-calibration.mjs --job <job.json> --out evals/fixtures/calibration/2026-07-17-fable-blind-weave-onset-energy.jsonAn acoustic or onset REJECT writes typed evidence before exit and withholds any
stale or new HTML. A TTS reject attempts frozen-take cleanup through D6d and uses
the fresh-take reroll/build runbook shown above. A BYO reject has no TTS cache to
reroll, so its runbook instead names the scene and requires replacing or editing
its audioWav source before rerunning synthesis and BUILD. The shared D6d purge
boundary skips BYO artifacts even if they carry a stale TTS cache receipt, so
no acoustic, transcript-fidelity, onset-energy, or direct BUILD caller can purge
a prior TTS take for that BYO scene. Thresholds are install constants; there is
no per-run weakening flag.
Replay the eval (deterministic and offline; no TTS daemon/network, but ffprobe
is needed for mp3 duration checks):
bun test evals/audio-dashboard.test.mjsReject any dashboard with WPM/even-split/estimated timing, placeholder recap
copy, build-aftercode-cinema.mjs, golemplaylist-base.js, direct
dashboards-serve output, empty words, missing word-click seek, or missing
hosted delivery or authenticated account-sync proof.
Transfer & Update (getting this skill to another machine)
This skill travels as files only — do NOT clone narrationlayer / agent-html
/ skill-creator onto the target machine. Use the golems skill-sync path (NOT
/sync-to-mac, which is for the notarized Layers app):
- Transfer — the skill lives at
$GOLEMS_ROOT/skills/golem-powers/audio-dashboard/. Getting it onto another machine = get that directory there, by whichever golems mechanism is in use:gitthegolemsrepo on the target (the skill is committed under it), orrsync -a "$GOLEMS_ROOT/skills/golem-powers/audio-dashboard/" <host>:<golems-root>/skills/golem-powers/audio-dashboard/for a repo-free copy.
- Register — committed ≠ installed. Run
golem-install/setup-symlinkson the target (it auto-discoversgolem-powers/*/and symlinks each into~/.claude/skills,~/.agents/skills,~/.codex/skills). Then verifyaudio-dashboardappears in the skill list of a fresh session. - Bootstrap — run
bun scripts/bootstrap.mjson the target; install any missing HARD deps it reports. For TTS, start the qwen3 daemon or use BYO-audio / a direct.wavreference. Update — re-transfer the directory (step 1) and re-run the eval (
bun test evals/audio-dashboard.test.mjs). Because the engine is vendored, updating the skill = updating these files; there is no separate engine repo to keep in sync. Re-vendor from source only when the upstreamnarrationlayer/agent-htmlengine changes and you deliberately pull it in.NarrationLayer maintenance is governed by the schema-v1 paired-hash manifest at
vendor/narrationlayer/VENDOR-VERSION. Run the two-way comparison against an explicit checkout before and after any synchronization:NARRATIONLAYER_UPSTREAM=/path/to/narrationlayer bun scripts/lint-narration-vendor-drift.mjsResolve stamped debts only from a clean committed upstream. The refresh command checks every required source marker, runs each debt's targeted upstream test, recomputes both sides of every pair, preflights the resulting manifest, and then replaces the stamp atomically:
Manifest
testCommandentries and whole debt-pair blessing form a reviewed-manifest trust boundary: same-PR review must verify the command and every byte riding inside the declared pair before resolution.NARRATIONLAYER_UPSTREAM=/path/to/narrationlayer bun scripts/lint-narration-vendor-drift.mjs --refresh-stamp --resolve-open-debtUpstream-first closures also require the selected vendor files to be committed;
resolvedVendorCommitidentifies those exact bytes. To leave an unmet debt visible while closing supported debts, append--keep-open-debt <debt-id>(repeat for multiple debts). A deferred debt stays OPEN and REJECTED. Only selected, declared debt pairs may move; unrelated drift still refuses refresh.If the upstream path is missing, the lint emits a typed
UPSTREAM_UNAVAILABLErecord with valuestamp-onlyand verdictDEGRADED. That is deliberately loud evidence that only vendored hashes were checked; never report it as a full upstream comparison. The separate agent-htmlcinema.jsprovenance debt remains on its existing ledger and is not covered by this NarrationLayer stamp.
Supersedes
This skill is the one canonical read-along audio-dashboard path. It supersedes the following classes of prior artifact (do not resurrect them):
- GolemPlaylist V1 / multi-audio players — e.g.
golemplaylist-base.jsand the old per-episode learning dashboards +build-static-site.mjs. - V4/AfterCode builders without real whisper timing — e.g.
build-aftercode-cinema.mjsand any cinema built from WPM/estimated timing. - External one-off synth/render scripts the vendored engine now replaces:
aftercode-tonight-synth.ts,regen-*-real-word-timings.ts, andbuild-aftercode-tonight.mjs. - Podcast/card/research dashboards that are not read-along, narrationlayer forks, and VoiceBar player UIs — out of scope.
The exhaustive per-file list of superseded artifacts (episode-specific) lives in git history of this file, not here — this section stays pattern-level and portable.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

