# Kiwi AI - how to produce a video

Source: the approved test video "ai-news-kiwi-test-v6.mp4" (OpenAI, 3 posts, 66.3s: story 60.7s + outro 5.6s, 720x1280). Follow this pipeline exactly. Reference renderer: pipeline/render_ai_news.py.

## Hard rules (from the channel owner)
1. English only: narration and captions.
2. One company per video.
3. At most 5 posts per video, and more than one (not boring). The test used 3.
4. Every picture is a real screenshot of an official post. No generated images, no AI art, no stock images.
5. Explain how a viewer can use the new feature now, to improve with AI (plan, where to click, what to try).
6. Word-by-word highlighted captions, same style as the Fluent Fox videos.
7. Only the Kiwi AI logo (assets/kiwi-ai-logo-curved.png) sits in a circle in the top-right corner of every frame, small, so it never covers the post. No company mascots, no other logo.
8. No Fluent Fox logo, no "Stay fluent. Stay sharp." ending. Every video ends with the fixed Kiwi AI outro (Step 7b): do not change it, do not add anything after it.
9. Do not log in anywhere, do not post anything. Use only logged-out x.com pages.
10. Only recent official posts (last day or two) that announce a user-facing feature.
11. LENGTH: every finished video, outro included, is at least 70 seconds (1:10). Hard requirement, checked in Step 9. The story alone must be at least 64.4 s (outro is 5.6 s). Reach it with more official posts (up to 5) or fuller step-by-step how-to for each post, never with filler or silence, about 175-190 words of narration.

## Step 1 - pick the posts
- Choose the company, then 2-5 recent official posts on its official X accounts (example: @ChatGPT and @OpenAI).
- Open every status URL yourself on x.com (logged out) and confirm account, text, date and view count. A third-party page that merely embeds a post is not proof of an official post.
- Keep a source list: URL, account, date/time, views.

## Step 2 - screenshots (cloud browser, no login)
- Acquire a cloud browser lease (read mode), navigate to the status URL.
- With execute-js: set document.body.style.zoom='0.82' (so tall posts with a video fit inside the 1280x720 viewport), scrollTo(0,0), remove fixed/absolute banners at the bottom ("Continue to X", "See all the replies", login nag), then read the bounding rect of the first <article>. It must end above the bottom of the viewport.
- Take a screenshot (save=true, PNG) and crop to the article rectangle with a small margin using PIL. Account for zoom and devicePixelRatio when converting the rect to pixels.
- Look at every crop: text readable, date and views visible, no banner covering it.

## Step 3 - script
- About 175-190 words, about 12-14 short sentences, plain English, so the finished video (outro included) is at least 70 s.
- Opening: "Here are N things <Company> shipped this week, and exactly how you can use each one right now."
- For each post: what it is in one sentence, then how to use it today (which plan, which app, where to tap, one concrete thing to try). Say availability honestly (plans, platforms).
- Close with: "N upgrades, all live." Do NOT say or write "Follow for tomorrow's AI news." in the story: that line belongs only to the fixed outro (Step 7b), so it is heard once.
- Test video: 3 posts, 66.3 seconds (story 60.7 s + 5.6 s outro). The channel minimum is now 70 s including the outro (rule 11), so this test video is shorter than the rule.

## Step 4 - narration
- Voice: Aoede, clear and expressive, generated with the speech tool by the main agent (task agents cannot run it). Ask the main agent for the WAV, give the exact script text and say: no director notes (the safety filter rejected them before).
- The script is fixed once the audio exists.

## Step 5 - word timestamps
- Local faster-whisper: model small.en, compute_type int8, cpu_threads=2, word_timestamps on (pip install --user faster-whisper).
- Real per-word start/end. Never allocate equal time per word, never interpolate a missing word.
- The renderer asserts that the number of words and the normalized text (lowercase, letters and digits only) match the script. If whisper splits a token (example: "1.1" became "1" and ".1"), rewrite the word text in word_times.json to the display tokens but keep the observed start/end times, and check the display again.
- After the final render, transcribe the finished audio again and compare with the script word by word.

## Step 6 - story.json and render
story.json fields: sentences (list of strings), scene_of_sentence (index of the screenshot shown for each sentence), images (post PNG paths), word_times (JSON path), audio (WAV path). Paths are relative to story.json. Example from the test: 10 sentences over 3 images, scene_of_sentence = [0,0,0,0,1,1,1,2,2,2] (the story ends on "Three upgrades, all live."; the call to follow is only in the outro). Whisper words are grouped into sentences by the trailing period.

Run: python3 render_ai_news.py story.json out.mp4 (optional --start S --end E renders a silent chunk; needed because one tool call is limited to about 2 minutes: render 0-22s, 22-44s, 44-end as chunks, concat them with ffmpeg, then mux the audio with: -af adelay=300:all=1,apad -shortest -c:a aac -b:a 128k -movflags +faststart).

Look of the render (all in render_ai_news.py):
- 720x1280, 24 fps, H.264 + AAC, faststart. The test file was 3.5 MB, far below the 16 MB WhatsApp video limit.
- Background: vertical navy gradient (16,30,58) to (10,20,42).
- Post card: width 664 px (tall video-post screenshots are scaled down to at most 740 px high so the card never reaches the captions), rounded corners, soft shadow, slow zoom 1.0 to 1.045 over its scenes, top about 70 px.
- Cut to the next post 0.35 s before the sentence onset, 0.5 s crossfade. Narration starts 0.3 s after the first frame.
- Captions at the bottom: DejaVu Serif Bold 38, spoken words cream (250,243,222), current word gold (255,200,60) highlight with navy text, upcoming words dim (170,178,196), sentence progress dots under the text. Keep text inside the phone safe area.
- End of the story: the renderer holds 0.1 s after the last word (OUT0 = last word end + 0.1). Then, on the story only, apply a 0.25 s video fade-out and a 0.2 s audio fade-out ending at that cut (ffmpeg fade=t=out and afade=t=out), so the outro follows right away. Do not hold 1.4 s.

## Step 7 - Kiwi AI logo in the corner
- The only corner logo is the Kiwi AI logo, assets/kiwi-ai-logo-curved.png (already a circle with a transparent outside). Do not use company mascots.
- Overlay with ffmpeg on the story video (after the end fade, before the outro is joined; the outro file already has its own corner logo) (or run pipeline/add_logo.sh video.mp4 out.mp4), size 104 px, 14 px from the right edge and 10 px from the top:
  ffmpeg -i video.mp4 -i kiwi-ai-logo-curved.png -filter_complex "[1:v]scale=104:104[l];[0:v][l]overlay=W-w-14:10" -c:a copy -preset veryfast out.mp4
- A larger logo (150 px at 24 px margin) covered the post text and was rejected. Always extract frames (for example at 2 s, 10 s, 30 s, 55 s) and check the circle does not cover any post text.

## Step 7b - fixed Kiwi AI outro (appended to every video)
- Reuse assets/kiwi-outro-approved-v1.mp4 unchanged (5.6 s, with audio), or re-render it with pipeline/render_outro.py (needs assets/kiwi-outro-sunglasses.png and the corner logo). Same design as the Fluent Fox ending: navy gradient, the circled sunglasses kiwi (a black disc, margin included, NOT the corner logo), a row of Like, Subscribe and bell buttons, an animated cursor that clicks them one by one (Like turns blue "Liked", Subscribe turns red "Subscribed", the bell lights up), then the call to action.
- Narration (Aoede): "Like, subscribe, and tap the bell. Follow for tomorrow's AI news." It starts 0.4 s into the outro. On screen: "Follow for tomorrow's" (cream) and "AI news." (gold). Clicks at 0.75 s (Like), 1.35 s (Subscribe), 2.45 s (bell); text at 3.05 s and 3.85 s; fade to black over the last 0.25 s.
- The corner logo stays visible during the outro. Join with ffmpeg concat (re-encode H.264/AAC, 24000 Hz audio, faststart):
  ffmpeg -i story.mp4 -i kiwi-outro-approved-v1.mp4 -filter_complex "[0:v:0][0:a:0][1:v:0][1:a:0]concat=n=2:v=1:a=1[v][a]" -map "[v]" -map "[a]" -c:v libx264 -preset fast -crf 22 -pix_fmt yuv420p -c:a aac -b:a 128k -ar 24000 -movflags +faststart final.mp4
- Add no channel name, no "The End", no extra slide after it.

## Step 8 - check before delivery
- Look at frames at the start, middle, every scene change and the end.
- Audio stream present, duration, file under 16 MB, the finished audio re-transcribed equals the script.
- Deliver the video with the source list. Do not post or upload anywhere.

## Step 9 - caption/voice sync check (MANDATORY, last step before delivery)
- After the final render and before sending, verify caption and voice sync carefully at many points. Do not skip this step.
- Word times: align whisper tokens to the script by characters (not by token count), because whisper splits or mis-hears tokens ("3.5", "GPT-6", "Max" as "Mac", "SuperVL"). Never map greedily. Words that follow a pause start at the pause end (ffmpeg silencedetect, -35 dB, 0.2 s).
- Caption lead: the renderer shifts captions 0.12 s earlier than the voice (CAP_LEAD). Captions must never lag the voice.
- Check 1 (numbers): transcribe the final audio with a second model (base.en, word timestamps) and compare with the highlight changes found in the frames (gold highlight pixels, 24 fps, crop the whole caption panel, which can be 4 lines tall). Pass: median difference within 0.1 s (captions slightly early is fine), no word more than 0.4 s off.
- Check 0 (length): ffprobe the final file. Duration must be 70.0 s or more including the outro. If shorter, add posts or fuller how-to steps, get new narration, and redo everything.
- Check 2 (eyes): extract at least 8 frames at the middle of chosen words, spread over the whole video, and confirm the highlighted word is the word being spoken.
- If a check fails, fix the word times, re-render and run both checks again.
