← back to Allnewsdaily

scripts/short/CONTRACTS.md

83 lines

# allnewsdaily daily-Short pipeline — shared interface contract (TK-11342)

All stages live in `~/Projects/allnewsdaily/scripts/short/`. Generated media goes in
`~/Projects/allnewsdaily/data/short/` (gitignored — do not commit media/artifacts).
Node is v26 (built-in `fetch` available; **use raw fetch, add no heavy npm deps**).

## INPUT — `~/Projects/allnewsdaily/data/wire.json` (already exists, refreshed every 2 min)
```
{
  updatedAt: ISO,
  splash: { outlet, link, date, topic },              // the LEAD story
  columns: [ { key, title, items:[ {outlet,link,date,topic} ], ok, total } ]  // 3 columns
}
```
**CRITICAL, verified 2026-09-09:** there is **NO `title` field**. `item.topic` **IS the
headline sentence** (a full sentence, e.g. "Google has announced a €13 billion investment…").
`item.outlet` = source name. `item.link` = article URL. `item.date` may be RSS or ISO format.

## STAGE 1 — `pick-stories.js`  (owner: content subagent)
Read wire.json → select **6** stories: `splash` first, then the strongest items across the 3
columns. Dedupe by outlet (max 2 per outlet). Skip items whose `topic` is empty or < 25 chars.
Write `data/short/stories.json`:
```
{ date:"YYYY-MM-DD", generatedAt:ISO,
  stories:[ { n:1, headline:<item.topic, trimmed>, outlet, link, tag:<column title or "Top"> }, … 6 ] }
```
`node scripts/short/pick-stories.js` also prints the 6 chosen headlines.

## STAGE 2 — `build-script.js`  (owner: content subagent)
Read stories.json → build a **45–55s** narration for a vertical Short. FACTUAL news-brief tone,
**no opinion/editorializing**. Intro: "All News Daily. Your headlines for {Month D}." Each beat:
"{headline} — via {outlet}." Outro: "That's your briefing. Full stories and live coverage at
all news daily dot com." Pace ≈ 2.7 words/sec. Hard cap totalSec ≤ 58 (Shorts must be < 60s);
if over, drop to 5 beats. Write `data/short/script.json`:
```
{ intro:{text,estSec}, beats:[{n,headline,outlet,text,estSec}], outro:{text,estSec},
  totalSec, narration:"<full flat text, newline between segments>" }
```

## STAGE 3 — `render-short.js`  (owner: render subagent — the ffmpeg heavy-lift)
Read stories.json + script.json + `data/short/vo.mp3` (voiceover; if absent, render silent + WARN).
Produce `data/short/out.mp4`: **1080×1920, H.264/yuv420p, 30fps, < 60s, AAC audio**.
- Use **ffmpeg `drawtext`** (NOT node-canvas — avoid the native cairo build). Bundled font:
  `/System/Library/Fonts/Supplemental/Arial Bold.ttf` (fallback Helvetica).
- Per beat = a branded card: dark bg (#0a0a0a / subtle gradient), top wordmark **ALL NEWS DAILY**
  with a red accent bar, the **headline word-wrapped & centered** (wrap ≈ 22–26 chars/line),
  the **outlet** as a chip lower-third, and a subtle Ken-Burns zoompan.
- Sync card durations to the REAL VO length: `ffprobe` vo.mp3 for duration, distribute across
  intro+beats+outro proportional to each segment's estSec.
- Also emit `data/short/thumb.jpg` (a strong first/lead-card frame, 1080×1920).
- Robust: check ffmpeg exists; fail LOUD with a clear message. `node scripts/short/render-short.js`.

## STAGE 4 — `youtube-auth.js` + `upload-youtube.js`  (owner: upload/auth subagent)
Creds from `~/Projects/secrets-manager/.env`: `YOUTUBE_CLIENT_ID`, `YOUTUBE_CLIENT_SECRET`.
- **`youtube-auth.js`** — OAuth 2.0 **loopback** flow. Scope
  `https://www.googleapis.com/auth/youtube.upload` + `.../youtube.readonly`. Start a localhost
  server on `http://localhost:9964/oauth2callback`, PRINT the consent URL for Steve to open,
  catch the `code`, exchange for tokens, and SAVE `refresh_token` to
  `~/Projects/allnewsdaily/.env` as `YOUTUBE_REFRESH_TOKEN=` (that file is gitignored; note in
  output that it should also be routed via the `secrets` skill). On success, call
  `channels.list?mine=true&part=snippet` and PRINT the authorized channel title (confirms WHICH
  channel). If token exchange returns `redirect_uri_mismatch`, print a clear instruction that
  `http://localhost:9964/oauth2callback` must be added as an authorized redirect URI on the
  OAuth client in Google Cloud Console. Raw fetch, no googleapis dep.
- **`upload-youtube.js`** — export `async uploadShort({file,title,description,tags,privacyStatus='unlisted'})`.
  Refresh an access token from `YOUTUBE_REFRESH_TOKEN`, then do a **resumable** upload: POST
  `https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status`
  with JSON metadata (snippet.title/description/tags/categoryId=**25** News&Politics;
  status.privacyStatus; status.selfDeclaredMadeForKids=**false**), then PUT the file bytes to the
  returned `Location`. Return `{videoId, url}`. CLI: `node scripts/short/upload-youtube.js --file
  data/short/out.mp4 --title "…" --dry-run` (dry-run prints metadata, no upload; used for testing
  before the OAuth token exists).

## STAGE 5 — `make-daily-short.js` (orchestrator — owned by the merge step, do NOT build)
Chains 1→2→ElevenLabs VO→3→4, logs cost, writes the daily canary `data/latest.json`
(PASS/WARN/FAIL). The subagents do NOT build this; leave it to integration.

## Rules for all subagents
- Work under **TK-11342**; `tk log TK-11342 "…"` your actions. Do NOT `git commit` (merge step commits).
- Self-test what you can WITHOUT spend or the OAuth token (dry-runs, silent-VO render, unit checks).
- No editorializing in any narration/metadata — this is a news channel; facts + attribution only.
- Do not fetch or embed the outlets' own images/video (copyright) — our branded cards only.