# TT-CLIPS Daily Runbook — podcast → TikTok Creator Rewards clips

Goal each day: **5–10 new clips (each ≥ 65 s)** from one approved podcast, vertical 1080×1920, captions burned in, upscaled, plus a posting page with hook / caption / description / 10 hashtags / transcript. A human posts from the page.

This is written for an agent (Claude Code or similar) on this Mac. Everything lives in `/Users/walterbarr_1/TT-CLIPS`.

## 0. Fixed rules (from the Skool course — do not violate)
1. Every clip **≥ 60 s** (target 65–95 s). Sub-minute clips don't earn.
2. One podcast per TikTok page. Don't split.
3. A podcast is only eligible if **nobody posts 1-min+ clips of it on TikTok** (their own account included). Re-check monthly.
4. Source must post **weekly**. Check last upload date.
5. Credit the source on screen (`credit` in `group.json`) — already burned in by the build.
6. Titles 5–7 words, framed to split opinion. Hooks in the first 2 seconds.

## 1. Machines / tools (already installed)
| Thing | Where | Notes |
|---|---|---|
| Downloader | `yt-dlp` (pyenv shim), needs `--js-runtimes node` and the HLS format string below | Update with `pip3 install -U yt-dlp` if 403/"format not available" |
| Transcription | `transcribe.py` (faster-whisper small, CPU, word timestamps) | ~10 min per 40-min episode; run 4 in parallel |
| Editing | `ffmpeg` (9.x, no subtitles) + `/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg` (8.x, has `ass` filter) | build scripts already pick the right one |
| Face-follow crop | OpenCV haar cascades inside `build_podcast_clips.py` | multi-cam studio shows |
| Upscale | Hilbert Windows box via `~/LTX-TT/pipeline/hilbert_post.py` (Real-ESRGAN → 1080×1920) | ~6–9 min per clip. Tailscale 100.108.35.71, password in `~/LTX-TT/.env` (`HILBERT_PASS`, use `sshpass -e`, never print). **Keep the Mac awake** (`caffeinate -i`) or the hand-off stalls; finished files wait in `C:/hilbert-video/outbox/` and can be pulled with scp. |
| Posting page | `make_page.py` | static HTML, serve with `python3 -m http.server 8777` |

HLS format string that works (YouTube blocks the normal one without cookies):
```
-f "bv*[height<=1080][protocol*=m3u8]+ba[protocol*=m3u8][language=en]/bv*[height<=1080][protocol*=m3u8]+ba[protocol*=m3u8]/b" --merge-output-format mp4 --js-runtimes node
```
Check the merged file has **English** audio (`ffprobe … stream_tags=language`); long videos sometimes grab a dubbed track.

## 2. Folder layout (one folder per podcast = "group")
```
TT-CLIPS/
  <group>/            e.g. beauty/, christian/ (root-level files = the John Ramirez batch)
    group.json        {"credit":"@handle","source_youtube":...,"source_tiktok":...}
    src/              <videoId>.mp4 + <videoId>.json (transcript)
    clips.json        [{"n":1,"vid":"<videoId>","start":s,"end":s,"title":"..."}...]
    work/             _cut / _raw / _up / .ass / crop cmd files (disposable)
    clips/            final mp4s, index.html, transcripts/, POSTING_SHEET.md
    posts.json        hook/caption/description/hashtags per clip (feeds make_page.py)
  build_podcast_clips.py   cut → face-follow crop → Hilbert upscale → captions
  build_clips.py           older fixed-crop version used for John Ramirez (single webcam speaker)
  transcribe.py, make_page.py, RUNBOOK.md, README.md (podcast lists), COURSE_NOTES.md
```

## 3. Daily procedure
**Step 1 — Source check (2 min).** For the group's channel:
```
yt-dlp --no-warnings --js-runtimes node --flat-playlist --playlist-end 6 --print "%(id)s|%(upload_date)s|%(duration)s|%(title)s" https://www.youtube.com/@<handle>/videos
```
Pick episodes not yet in `src/` (keep a `done.txt` of used video ids). If the newest upload is >14 days old, flag "source going stale" in the report.

**Step 2 — Monthly eligibility re-check** (first run of the month):
```
yt-dlp --no-warnings --flat-playlist --playlist-end 25 --print "%(duration)s" https://www.tiktok.com/@<handle>
```
If any ≥ 60 → the seat is taken; stop and pick a new podcast from `README.md` (run the same check on candidates). TikTok *search* for other clippers needs a logged-in human — ask the user to do it in the app.

**Step 3 — Download + transcribe** (background, ~20 min for 4 episodes):
```
cd <group>/src && yt-dlp <format string above> -o "<id>.mp4" "https://www.youtube.com/watch?v=<id>"
python3 ../../transcribe.py <id>.mp4 <id>.json
```

**Step 4 — Pick moments.** Per episode, read the transcript and choose 2–4 segments, 65–95 s, sentence-aligned, with a hook (myth busted, number, confession, hot take, "worth it or not"). Subagents work well here: one per transcript, return JSON `{start,end,title,hook,why}`. Write the chosen set to `clips.json` (n = 1..N). Target 10 clips/day = ~2 days of posting at 5/day.

**Step 5 — Build** (≈ 8 min per clip, mostly Hilbert; run under `caffeinate -i`):
```
caffeinate -i python3 build_podcast_clips.py --group=<group>            # all clips
python3 build_podcast_clips.py --group=<group> --skip-upscale --only 3  # quick preview of one
```
Then eyeball: make a 6-frame contact sheet per clip (`ffmpeg -vf "select=...,scale=270:480,tile=6x1"`) and check face framing + caption text. Fix obvious transcription errors via `FIXES` dict (e.g. "product"→"prodigal") and rebuild captions (delete the final mp4 only; `_up` is kept).

**Step 6 — Copy.** Write `posts.json`: for each clip → `hook` (≤ 12 words), `caption` (1–2 lines, ends with a question or emoji), `description` (2–3 sentences incl. source credit), exactly **10 hashtags** (niche + broad + source name), `len`, `ass` path. Then:
```
python3 make_page.py <group> --name=<date>-podcast-<PageName>   # → clips/index.html (list) + clips/NN.html (one page per clip: video + description+hashtags copy box)
```

**Step 7 — Hand-off.** Serve and open: `python3 -m http.server 8777` → `http://127.0.0.1:8777/<group>/clips/index.html`. Send the user the contact-sheet image + the page link. Report: clips built, lengths, anything skipped, source staleness.

## 4. Failure playbook
| Symptom | Fix |
|---|---|
| yt-dlp 403 / "Requested format is not available" | `pip3 install -U yt-dlp`; make sure `--js-runtimes node`; use the HLS format string |
| Hilbert step hangs / Mac slept | `pkill -f hilbert_post`; list `C:/hilbert-video/outbox/` over ssh; scp the `<n>_raw_<ts>_1080x1920_30fps.mp4` to `work/<n>_up.mp4`; rerun build (it skips done stages) |
| Face crop wrong (speaker cut off) | Check `work/<n>_crop.cmd` keyframes; raise `jump` or widen `CW`; for single-webcam sources use the fit layout in `build_clips.py` (`crop=None`) |
| Caption filter error "No option name near" | wrong ffmpeg — the caption stage must use `ffmpeg-full` |
| Wrong-language audio | re-download with `[language=en]` format string; check `ffprobe … stream_tags=language` |
| Clip < 60 s after trimming | extend `end` to the next sentence boundary; never ship under 60 s |

## 5. What a "done" day looks like
- `<group>/clips/` has N new mp4s, all 1080×1920, 30 fps, ≥ 60 s, `+faststart`.
- `index.html` lists them; each `NN.html` has the video and one copy box (description + 10 hashtags).
- Contact sheet reviewed (faces framed, captions readable, title readable).
- `done.txt` updated with the video ids used; report sent to the user.

## 6. Where the pages live (any device on the Tailscale network)
- **https://walters-macbook-pro-6.tail10414c.ts.net/** → landing page linking every posting page. NOW PUBLIC via Tailscale Funnel (user requested 2026-08-24) — unlisted URL, don't promote it; turn public off with `/Applications/Tailscale.app/Contents/MacOS/Tailscale funnel --https=443 off` (tailnet-only serve stays).
- Served by a launchd agent `com.wally.ttclips` (python http.server on 127.0.0.1:8777, working dir TT-CLIPS) + `tailscale serve --bg --https=443 http://127.0.0.1:8777`. Both auto-start; the Mac must be awake and online.
- New day: add a line to `TT-CLIPS/index.html` pointing at `<group>/clips/<name>.html`.
- To stop sharing: `/Applications/Tailscale.app/Contents/MacOS/Tailscale serve --https=443 off`.
