Skip to content
Open
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
36 changes: 36 additions & 0 deletions brands/things-worth-doing/brand.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "Things Worth Doing",
"description": "Field guide / documentary / editorial-magazine aesthetic for a men's competence-and-doing content brand. Deliberately avoids neon, gamer, generic-AI-futuristic, and overly polished corporate looks — see the vault's Brand Strategy and Visual Identity sections.",
"version": "1.0.0",

"colors": {
"primary": "#B5651D",
"primaryLight": "#D98C4A",
"textDark": "#2A2620",
"textMedium": "#4A4438",
"textLight": "#8A8172",
"bgLight": "#F4EFE6",
"bgDark": "#171410",
"bgOverlay": "rgba(23, 20, 16, 0.85)",
"divider": "#3A352B",
"shadow": "rgba(0, 0, 0, 0.35)"
},

"fonts": {
"primary": "Arial, Helvetica, sans-serif",
"mono": "ui-monospace, Menlo, monospace"
},

"spacing": { "xs": 8, "sm": 16, "md": 24, "lg": 48, "xl": 80, "xxl": 120 },
"borderRadius": { "sm": 7, "md": 12, "lg": 16 },

"typography": {
"h1": { "size": 88, "weight": 700 },
"h2": { "size": 72, "weight": 700 },
"h3": { "size": 48, "weight": 700 },
"body": { "size": 44, "weight": 400 },
"label": { "size": 34, "weight": 500, "letterSpacing": 3 }
},

"_proposed_default": "Colors are a proposed default matching the 'field guide + documentary' visual identity (warm rust/amber accent, warm off-black background, warm off-white text) — not yet confirmed by Adrian. Change freely; nothing downstream depends on these specific hex values."
}
22 changes: 22 additions & 0 deletions brands/things-worth-doing/voice.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"voiceId": "cjVigY5qzO86Huf0OWal",
"description": "Voice Brief: calm, confident, conversational, knowledgeable, slightly understated (never announcer-register).",
"settings": {
"stability": 0.68,
"similarityBoost": 0.85,
"style": 0.15,
"useSpeakerBoost": true
},
"model": "eleven_multilingual_v2",
"qwen3": {
"speaker": "Ryan",
"language": "Auto",
"tone": "",
"instruct": "",
"clone": null
},
"sixtydb": {
"voiceId": "YOUR_VOICE_ID_HERE",
"settings": { "stability": 0.85, "similarity": 0.9, "speed": 1.0, "enhance": true }
}
}
74 changes: 74 additions & 0 deletions templates/things-worth-doing-short/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# things-worth-doing-short — guidance for Claude

Forked 2026-09-01 from `templates/concept-explainer-short` for the "Things Worth
Doing" men's content brand ([[Men's UGC Project]] in the Obsidian vault, and
[[Playbook - AI Agent Content Sprint (Things Worth Doing)]] for how a batch of
scripts/shot lists gets produced upstream of this template). Same Python/moviepy
pipeline, no Remotion, no npm — `gen_vo.py → gen_captions.py → build.py`, run
**from the project directory**. Differences from the base template are below;
everything not mentioned here works exactly like `concept-explainer-short`.

## What's different from concept-explainer-short

1. **One scene = one shot, not one narration beat.** The base template's own
guidance ("~15s pattern interrupt") assumes few, longer scenes. Things Worth
Doing's Production Standard requires visual movement every few seconds, so
TWD scenes run ~2-4s each — map each shot in the sprint playbook's Shot List
directly to one `scenes.json` scene, in order, one VO line per shot. A
28-second video is normal at 7-9 scenes here, not 4.
2. **`visual` field gets a third real value: `"real"`.** `build.py` actually
decides treatment by file **extension**, not by the `visual` string — it's
documentation only. So a real filmed `.mp4` clip gets the exact same
boomerang-loop treatment an LTX clip would. Tag it `"real"` in scenes.json
purely so a human scanning the file can tell what still needs to be shot
vs. generated — `build.py` doesn't care.
3. **Voice is ElevenLabs by default**, via `config.json → voice.brand:
"things-worth-doing"` (patched into this fork's `gen_vo.py` — the base
template didn't forward `--brand` to `tools/voiceover.py` at all, only
scene text and qwen3-specific fields). Real voiceId is still a placeholder
in `brands/things-worth-doing/voice.json` — see Gotchas.
4. **Palette is the proposed "field guide + documentary" default** in
`config.json`/`brands/things-worth-doing/brand.json` — warm rust/olive
accents, warm off-black/off-white, deliberately not neon or AI-futuristic
(matches the brand's Visual Identity). Not yet confirmed by Adrian; safe to
change, nothing else depends on the specific hex values.
5. **Real footage is expected, not optional.** TWD's playbook prioritizes real
product/filmed footage over AI generation wherever the shot needs to show
the actual product being used (see the playbook's visual hierarchy). Don't
default to generating everything with LTX/Ideogram just because this
template can — check each scene's per-shot production package first.

## Working on a project copy (same as base template)

1. Plan in `scenes.json` first — one scene per shot from the video's
production package (VO Script + Shot List sections), not from scratch.
2. Hook discipline: scene 01 must earn the next few seconds — question or
tension immediately, no throat-clearing.
3. `build.py` works at every stage — placeholders before assets, estimates
before VO, silent before audio. Render early, render often; show the user
intermediate renders rather than describing them.

## Review checklist before calling a video done

- Pull frames with ffmpeg at several timestamps and *look* at them: caption
collisions, asset crops, placeholder cards left in.
- Check `gen_vo.py`'s per-scene wpm output; anything flagged FAST/SLOW that
`maxWpm` didn't catch needs a script edit or retake.
- Run this video's package through the sprint playbook's QC gates and
Red-Team pass again post-render, not just at pre-production — a render can
surface problems (bad continuity, a shot that doesn't land) invisible in
text form.

## Gotchas (this fork, so far)

- **`brands/things-worth-doing/voice.json`'s `voiceId` is still
`"YOUR_VOICE_ID_HERE"`** — `tools/voiceover.py` will error rather than
silently using a default. Replace it with a real ElevenLabs voice ID before
running `gen_vo.py` for real.
- Inherited from the base template: clone pacing follows the reference
recording, `voice.maxWpm` is a safety net not a fix; never burn whisper's
own transcription (captions force-align to script text on purpose); moviepy
2.x uses `with_*` methods and PIL for text.
- Keep toolkit-level fixes (improving this *template*) in
`templates/things-worth-doing-short/`, not in a copied project under
`projects/` — same separation the base template's CLAUDE.md establishes.
126 changes: 126 additions & 0 deletions templates/things-worth-doing-short/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# things-worth-doing-short

Forked from `concept-explainer-short` for the "Things Worth Doing" brand — same
mechanics documented below, but **see `CLAUDE.md` in this folder for what's
actually different** (scene cadence, real-footage handling, ElevenLabs/brand
wiring, palette). Read that first.

---

A 9:16 vertical **concept explainer** template — TikTok / Reels / YouTube Shorts
style — built entirely from AI-generated parts and composed with a single Python
build (no Remotion/Node). One concept, hook → explanation → payoff → CTA, with
big burned karaoke captions for sound-off viewing.

Everything is driven by **`scenes.json`**: each scene is narration text plus a
visual asset. The pipeline turns that into per-scene cloned (or built-in) TTS,
word-perfect caption timing, and an audio-anchored composite where timing drift
is impossible.

```
scenes.json ──► gen_vo.py ──► gen_captions.py ──► build.py ──► out/short.mp4
│ │
▼ ▼
audio/scenes/*.mp3 captions/words_*.json
vo_durations.json
```

## Quick start

```bash
cp -r templates/concept-explainer-short projects/my-short
cd projects/my-short

# 0. Renders immediately — placeholder cards, estimated timing, silent
uv run build.py

# 1. Write your scenes (see scenes.json for the example)
# 2. Generate visuals (from the TOOLKIT ROOT — see "Visuals" below)
# 3. Voiceover → captions → final render (from the project dir)
uv run gen_vo.py
uv run gen_captions.py # needs: uv sync --extra whisper
uv run build.py
```

## scenes.json

```json
{
"title": "Why Is the Sky Blue?",
"scenes": [
{ "id": "01", "slug": "hook", "visual": "ltx",
"asset": "clips/01_hook.mp4",
"text": "Look up on a clear day..." }
]
}
```

- `text` — the narration, verbatim. Captions burn *this* text (whisper only
provides timing), so write it exactly as it should appear.
- `asset` — relative path; the extension decides the treatment:
image → slow Ken Burns zoom, video → boomerang loop cut to the scene,
missing → gradient placeholder (so you can render at any stage).
- `visual` — documentation of intent (`ideogram` / `ltx`); build.py goes by
the asset extension.

**Pacing budget:** narration ÷ 2.4 ≈ seconds per scene. A 60s short is
~140 words total; the 4-scene example is ~45s. Keep the hook under 3 seconds
of setup. Platform limits: YouTube Shorts ≤ 3 min, Reels ≤ 3 min,
TikTok ≤ 10 min — gen_vo.py and build.py print a warning past 3 minutes.

## Visuals

Generate from the **toolkit root**, save into the project's `images/` and
`clips/`. The intended split (see CLAUDE.md "FLUX.2 vs Ideogram 4"):

```bash
# Text-bearing cards (Ideogram 4, JSON captions; 9:16 = --resolution 1440x2560)
uv run tools/ideogram4.py --json caption.json --resolution 1440x2560 \
--output projects/my-short/images/02_scattering.png

# Motion b-roll (LTX-2; 576x1024 scales exactly to 1080x1920)
uv run tools/ltx2.py --width 576 --height 1024 --num-frames 161 \
--prompt "..." --output projects/my-short/clips/01_hook.mp4

# Optional music bed (looped + ducked automatically if present)
uv run tools/music_gen.py --prompt "..." --duration 120 \
--output projects/my-short/audio/music.mp3
```

Alternate cards and motion so the viewer gets a pattern interrupt every
~15 seconds. Ideogram for anything with legible text; LTX for atmosphere.

## Voice

`config.json → voice` selects the narrator:

```json
"voice": {
"provider": "qwen3", "cloud": "modal", "maxWpm": 165,
"refAudio": "ref/my-voice.m4a",
"refText": "Exact transcript of the reference recording.",
"speaker": "Ryan", "tone": ""
}
```

- **Clone**: set `refAudio` + `refText`. Use 12–25s of varied, full-sentence
speech *at narration pace* — the clone copies the reference's pace and
temperature cannot fix a rushed reference (see `/voice-clone`).
- **Built-in**: leave `refAudio` empty; `speaker` + `tone` apply.
- `maxWpm` is the pacing safety net: rushed takes are slowed in place with
pitch-preserving atempo. gen_vo.py prints per-scene wpm either way.
- `provider: "elevenlabs"` also works (uses your configured voice).

## Captions

Karaoke pills, 1–3 words at a time, timed from whisper word timestamps but
**force-aligned to your script text** so they're word-perfect. Tune position,
size, and chunking in `config.json → captions`; defaults clear platform UI and
the bottom rows of Ideogram list cards. Set `enabled: false` to skip.

## Re-rendering

Everything is idempotent and cached. Re-run `gen_vo.py --force` after script
edits (then `gen_captions.py` again), or just `build.py` after swapping an
asset or tweaking config. Timeline math always re-derives from
`vo_durations.json` — there is nothing to manually re-time.
Loading