Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .agents/skills/explainer-video/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Six cited notes, written for the [thirdshift](https://github.com/JacobStephens2/
- If the typed command differs from the run that produced the output, caption the panel "real output from …".
- Mark steps whose output wasn't captured "output not shown".
- Strip personal data (emails, tokens) before capture.
- **Literal reading.** Read each visual literally: striking through "no daemon" says there *is* one, so strike through only "daemon".
- **Literal reading.** Read each visual literally: striking through "no daemon" says there *is* one, so strike through only "daemon". An arrow from A to C labelled "blocked by" reads as "A is blocked by C"; label the target instead ("C blocked by A, B").
- **Text sizes at 1080p:**
- Labels: at least 28 px for executives and 24 px for engineers.
- Terminal: 24 px mono, about 118 columns in 1700 px.
Expand All @@ -38,8 +38,8 @@ Six cited notes, written for the [thirdshift](https://github.com/JacobStephens2/

- **Key:** `ELEVENLABS_API_KEY` stays in the environment, injected by a secrets wrapper where one exists; never print it. Secret managers sometimes return 503/504: retry up to 4 times with a 10 s sleep.
- **Starter tier** refuses `mp3_44100_192`; `tts.py` asks for `mp3_44100_128`. Check quota at `/v1/user/subscription`.
- **eleven_v4** largely ignores `voice_settings.speed`, so change pace by editing words. The default voice is Matilda, `XrExE9yKIg1WjnnlVkGX`.
- Write numbers the way they should be spoken ("six sixty-three").
- **eleven_v4** largely ignores `voice_settings.speed`. The default voice, Matilda (`XrExE9yKIg1WjnnlVkGX`), reads at about 135–145 wpm, below the 160–185 the research suggests, and pauses at every colon. Plan length at 140 wpm, and cut words or colons to shorten.
- Write numbers and initialisms the way they should be spoken ("six sixty-three", "T D D"). Add each one to `build.py`'s `CAPTION_FIXES` so the captions show the written form, and add a spoken phrase whose full stop isn't a sentence end ("a.m. Eastern") to `NO_BREAK`.

## Assembly beyond `build.py`

Expand All @@ -48,6 +48,7 @@ For cuts that need their own ffmpeg, such as a marketing cut with a music bed:
- **ffmpeg concat** resolves list paths relative to the list file, so write absolute paths.
- **Dynamic volume:** `volume='if(lt(t,X),0.05,0.2)':eval=frame` takes linear gains, not dB strings.
- **Silent cuts:** add a silent AAC track (`anullsrc`); feeds handle it better than no audio track.
- **Captions:** Chrome loads a `<track>` only over http, so serve the folder (`python3 -m http.server`).
- **Captions:** Chrome loads a `<track>` only over http, so serve the folder (`python3 -m http.server`). A server from an earlier session may already hold the port and serve another folder: if a new file returns 404, serve on a fresh port.
- **Scrubbing:** libx264 puts a keyframe about every 8 s, so a local player scrubs in coarse jumps. For a scrub copy, re-encode with `-c:v libx264 -g 15 -keyint_min 15 -sc_threshold 0 -c:a copy`: a keyframe every 0.5 s, at about twice the size.
- **Exit codes:** in `cmd | grep`, the pipeline takes grep's exit code. Use `set -o pipefail`, or check that the output file exists.
- **zsh:** an unmatched glob is an error that aborts an `&&` chain (e.g. `rm -rf review/*` on an empty folder). Use `find dir -type f -delete`.
10 changes: 5 additions & 5 deletions .agents/skills/explainer-video/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Make narrated, animated explainer videos. Use when the user wants a

# Explainer video

Scenes are HTML pages whose `render(t)` is a pure function of time. Headless Chromium screenshots each frame, and ffmpeg joins the frames, narration and music. Work in a copy of `templates/` in a fresh project directory (the scratchpad unless the user names one). Gotchas for each step are in [REFERENCE.md](REFERENCE.md).
Scenes are HTML pages whose `render(t)` is a pure function of time. Headless Chromium screenshots each frame, and ffmpeg joins the frames, narration and music. Work in a copy of `templates/` in a fresh project directory (the scratchpad unless the user names one), and run `npm install` there, as in every copied version. Gotchas for each step are in [REFERENCE.md](REFERENCE.md).

Leading words:
- **sourced:** every claim traces to a line in the product's docs or code.
Expand All @@ -17,12 +17,12 @@ Leading words:

1. **Ground.** Read the source of truth: README, guide, site pages and code. List the claims the video will make. *Done when* every planned claim has its file:line, and the user has picked the audience (engineers, executives, new users, public).
2. **Research the audience** with the `research` skill, unless a note in REFERENCE.md's "Research to reuse" covers it. *Done when* the note ends in checkable rules and a beat outline.
3. **Script** `narration.json`. Use one scene per idea, written conversationally, and speak every on-screen label. Overclaiming hides in "never", "only" and "free", and in speed or productivity numbers. A proof point is a real, dated artifact the viewer can open, such as an issue and its PR. *Done when* every sentence is **sourced** and the total length fits the audience.
3. **Script** `narration.json`. Use one scene per idea, written conversationally, and speak every on-screen label. Overclaiming hides in "never", "only" and "free", in a bare "the" over a set ("the skills are adapted…" when most are), in a passive that hides who gave a permission ("allowed to add it" rather than "its owner allowed it to add it"), and in speed or productivity numbers. A proof point is a real, dated artifact the viewer can open, such as an issue and its PR: name each by its title rather than its number, and give times in the viewer's time zone. *Done when* every sentence is **sourced** and the word count at 140 wpm fits the audience.
4. **Voice.** Generate per-scene audio with timestamps using `tts.py`, with `ELEVENLABS_API_KEY` in the environment. On a new project, audition voices with the user. Then run `gen_cues.py` to get `cues/NN.js` and `plan.json`. *Done when* every scene has audio, cues and a planned length.
5. **Harness.** Write `storyboard.md`: the rules plus a direction for each scene. Adapt `style.css` and `lib.js` to the look, and set `window.BRAND` for the corner wordmark. *Done when* a still of a throwaway scene shows the loaded fonts and the wordmark, with an element landing on its cue word.
5. **Harness.** Write `storyboard.md`: the rules plus a direction for each scene. Adapt `style.css` and `lib.js` to the look, and set `window.BRAND` for the corner wordmark. *Done when* each direction gives its cue words in the order they are spoken, checked against `cues/NN.js`, and a still of a throwaway scene shows the loaded fonts and the wordmark, with an element landing on its cue word.
6. **Build scenes** with background subagents, one per scene, or one per group of scenes that share a continuous diagram. Give each the storyboard and its cues, and have it check its stills with Read before rendering video. *Done when* every `out/NN.mp4` matches `plan.json` within 0.05 s.
7. **Review.** An independent subagent that hasn't seen the code judges stills of every scene against the source. It names any factual errors and at most one scene to fix. Check one frame of each new scene yourself as well. *Done when* the flagged fixes are made and every claim still holds against its source.
7. **Review.** An independent subagent that hasn't seen the code judges stills of every scene against the source, including one in each scene's final hold, after its last cue. It names any factual errors and at most one scene to fix. Check one frame of each new scene yourself as well. *Done when* the flagged fixes are made and every claim still holds against its source.
8. **Assemble** with `build.py`. It joins the scenes, places narration at each scene's lead, keeps music to the unnarrated head and tail, normalises loudness, and writes a WebVTT caption file and a player page. Make a poster from the strongest frame. *Done when* loudness reads −16 ±1 LUFS and the player loads its captions over http.
9. **Present** the video in a served index page: one card per video, newest version first, captions off by default. Open it in the user's browser. *Done when* the page is open and you've told the user what was checked and what wasn't. Review used stills, so no one has yet watched the video end to end with sound.
9. **Present** the video in a served index page: one card per video, newest version first, captions off by default. Add the card to an existing index page in place, since other sessions may be editing it. Open it in the user's browser. *Done when* the page is open and you've told the user what was checked and what wasn't. Review used stills, so no one has yet watched the video end to end with sound.

For a change to a finished video, make a **new version**: copy the project directory, change only what was asked, re-voice only the changed lines, and re-render the scenes whose cues or counters changed.
15 changes: 13 additions & 2 deletions .agents/skills/explainer-video/templates/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@
final/explainer.vtt (captions from the narration's word timings), and final/index.html
(a <video controls> player with the captions as a toggleable track).
"""
import json, os, subprocess
import json, os, re, subprocess

here = os.path.dirname(os.path.abspath(__file__))
os.chdir(here)
plan = json.load(open("plan.json"))
music = os.path.join(here, "music.wav") # from music.py

# The narration spells numbers and initialisms for the voice; captions show the written form.
# Each pair is applied to every caption's text, e.g. ("T D D", "TDD"), ("seven ninety-four", "#794").
CAPTION_FIXES = []
# Spoken phrases whose full stop is not a sentence end, so no caption breaks inside them, e.g. "a.m. Eastern".
NO_BREAK = []

def run(*args):
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", *args], check=True)

Expand Down Expand Up @@ -57,15 +63,20 @@ def stamp(s):
a = json.load(open(f"voice/{p['scene']}.json"))["alignment"]
chars, starts, ends = a["characters"], a["character_start_times_seconds"], a["character_end_times_seconds"]
off = p["start"] + p["lead"]
spoken = "".join(chars)
keep = {m.start() + j for phrase in NO_BREAK for m in re.finditer(re.escape(phrase), spoken)
for j, ch in enumerate(phrase[:-1]) if ch in ".?!"}
buf, b0 = "", None
for i, c in enumerate(chars):
if b0 is None and not c.isspace():
b0 = starts[i]
buf += c
stop = c in ".?!" and (i + 1 == len(chars) or chars[i + 1].isspace())
stop = c in ".?!" and i not in keep and (i + 1 == len(chars) or chars[i + 1].isspace())
soft = c in ",:" and len(buf.strip()) > 84 - 30 and (i + 1 == len(chars) or chars[i + 1].isspace())
if stop or soft or i + 1 == len(chars):
text = buf.strip()
for spoken_form, written in CAPTION_FIXES:
text = text.replace(spoken_form, written)
if text:
words, lines, line = text.split(), [], ""
for w in words: # wrap to two lines of at most ~42 characters
Expand Down
7 changes: 7 additions & 0 deletions .agents/skills/explainer-video/templates/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"private": true,
"type": "module",
"dependencies": {
"playwright": "1.64.1"
}
}
2 changes: 1 addition & 1 deletion skills-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"source": "JacobStephens2/skills",
"sourceType": "github",
"skillPath": "skills/explainer-video/SKILL.md",
"computedHash": "15c1004b1bd948ccce36d0bc2b28161dd9dd53d2f5e6ae0906e6912e3f96aeae"
"computedHash": "e0fd9d83109998604f6a2fae066d8b28e9e98967f605f22247dec943f1f4209f"
},
"git-guardrails-claude-code": {
"source": "mattpocock/skills",
Expand Down
Loading