---
name: pov-reaction-video
description: Build short-form POV reaction videos — a generated character reacting while timed on-screen text tells the story. Use for cope-reactions, text-thread POVs, comment replies, and any format where the text arrives in beats instead of sitting on screen as one static line. Drives Post Ripple's MCP server (generate_video + create_video).
---

# POV reaction videos

The format: **one continuous clip of a character reacting, with on-screen text that
arrives in timed beats.** No green screen, no filming, no video editor. The text is the
story; the footage is the reaction to it.

Two proven structures, both built with this skill:

| Structure | Beats | What the viewer does |
| --- | --- | --- |
| **Cope reaction** | a quoted insecurity holds on screen → the character reads it, reacts → text clears → the answer lands as he looks at camera | recognizes the cope, waits for the verdict |
| **Text-thread POV** | 3–4 short messages appear one at a time over the character on his phone | reads to the end because the last line is the punchline |

Both work because the *reveal* is withheld. A single static caption over a person
standing still has nothing to wait for.

## Prerequisites

- Post Ripple MCP server configured with `POSTRIPPLE_API_KEY` (`read` + `content:write`
  scopes; `publish` only if you want the agent to schedule).
- A **saved character** with `consistency: "reference"` — run `list_characters` and pick
  one whose `status` is `ready`. Descriptor-only characters drift between clips and will
  break a series.
- Run `whoami` first to resolve `organizationId` (or set `POSTRIPPLE_ORG`).

Credits: 1 `video_generations` per base clip + **1 `creations` per render — no matter
how many text beats** (`text_over_video` burns them all in one pass). A 4-message
text-thread POV costs 1 generation + 1 creation.

## Step 1 — generate the base clip

`generate_video` with `characterId` set. Describe **only the scene and the reaction arc** —
identity comes from the character's reference photos. ~10s, 9:16, silent, ~40s render.
Poll `get_video` until `status: "completed"`.

The base clip must contain the arc the text needs. Write the beats into the prompt:

```
Sitting on a dark couch at home in a black t-shirt, holding his phone up and reading
it, gives a small amused eyebrow raise and a slight disbelieving head shake, then
lowers the phone and looks directly into the camera with a calm confident reassuring
expression, warm lamp lighting, medium shot from chest up with headroom above and
below, mouth closed the whole time, silent, not speaking, no talking, no scene cuts,
one continuous locked-off scene
```

That single prompt produced the highest-performing clip in the account. Reusable as-is —
swap the setting, keep the structure.

Prompt rules that matter:

- **`mouth closed the whole time, silent, not speaking, lips together the entire time`.**
  Without this the model renders a mouth open mid-word and the clip reads vacant. This is
  the single most common regeneration cause.
- **`no scene cuts, one continuous locked-off scene`** — a cut mid-clip desynchronizes
  every text beat downstream.
- **`medium shot from chest up with headroom above and below`** — you need empty frame
  above and below the face to put text in.
- Name the reaction beats in order ("reads it → eyebrow raise → lowers phone → looks at
  camera"). The model follows sequence prompts well, and you'll time text to those beats.
- Avoid "flexing", brand names, and real people — the safety filter rejects them. "posing"
  and "squares his shoulders" pass.

**Keep good base clips.** A clean read → react → look-at-camera arc is worth more than the
finished video: every future comment-reply is one `text_over_video` render over the same
footage, no new generation.

## Step 2 — render all the text beats in one pass

`create_video` type `text_over_video` burns **every beat in a single render**: pass the
base clip as `sourceVideoId` and the beats as a `texts` array — each entry has its own
`window`, `position`, and `style`. One `creations` credit, one poll, no chaining.
(Requires postripple MCP ≥ 0.21.0; older servers only offer the hook_demo caption-burn
workaround of feeding each render back in as the next `hookId`.)

```js
create_video({
  type: "text_over_video",
  sourceVideoId: BASE_CLIP_ID,
  texts: [
    {
      text: "“i’m 5’9 it’s over for me”",
      window: { startMs: 0, endMs: 4500 },
      style: { font: "classic", treatment: "outline", fontScale: 0.85,
               align: "center", x: 0.5, y: 0.16 },
    },
    {
      text: "nobody is measuring you bro",
      window: { startMs: 7000, endMs: 10000 },
      style: { font: "classic", treatment: "outline", fontScale: 0.85,
               align: "center", x: 0.5, y: 0.72 },
    },
  ],
})
// poll get_creation -> resultVideoId is the finished video
```

Rules:

- **Windows are on the output timeline** (after `trim`, if you pass one). A beat with no
  `window` stays on screen for the whole video.
- **Non-overlapping windows show one beat at a time.** Overlap is now a *tool*, not a bug:
  to stack escalating copes, let each earlier window run until the payoff (`0–6000`,
  `2000–6000`, `4000–6000`) instead of chaining renders.
- **Same `x`/`y` for every message in a thread** — a subtitle rhythm. Vary position only
  when the beats are deliberately different roles (quote on top, answer on bottom).
- **One line, ≤ ~30 characters.** At `fontScale: 0.85` the renderer wraps at ~33 chars and
  orphans ugly tails. `\n` now works as a hard line break (each line wraps and centers
  independently) — use it for deliberate two-line beats, not to rescue an overlong one.
- **Use curly quotes and apostrophes** (`“ ” ’`). They render correctly and dodge escaping
  risk entirely.

### Timing the beats

Divide the clip evenly for a text thread; anchor to the footage for a reaction.

```
Text-thread POV, 10s / 4 messages:  0–2500 · 2500–5000 · 5000–7500 · 7500–10000
Cope reaction, 10s:                 cope 0–4500 (holds through the reaction)
                                    payoff 7000–10000 (lands as he looks at camera)
Stacked cope, 10s:                  0–2000 · 2000–4000 · 4000–6000 (all at y 0.16)
                                    payoff 7000–10000 at y 0.72
```

**The stacked variant is the strongest shape in this family.** Three escalating copes at
the top, then one payoff that reframes the whole list instead of answering a single line
("i'm 5'9" → "i'm balding at 22" → "it's over for me" → *only you see the list bro*). It
reads better on the same footage because the character's reaction builds across the list
rather than firing once — and since `text_over_video` renders all beats in one pass, it
costs the same single creation. The proven shape replaces each cope at the same `y`;
a literal on-screen stack (earlier windows running until the payoff, each at its own `y`)
is now also possible in one render, but frame-check it — it hasn't been performance-tested.

**Time the payoff to the footage beat, not to the middle of the clip.** The first build of
the cope reaction put the answer at 5.2s while the phone was still raised — it collided
with the phone and clipped the first word. Moving it to 7.0s, the moment he lowers the
phone and meets the lens, fixed the overlap and the punch at once. Frame-check at the
**window boundaries**, not just mid-window.

## Step 3 — verify every pass visually

Non-negotiable. The API reports `completed` for renders that are visually broken.

```sh
curl --max-time 120 -o out.mp4 "<resultVideoUrl>"                 # curl, not urllib
ffmpeg -ss 8.2 -i out.mp4 -frames:v 1 -pix_fmt yuvj420p frame.jpg # yuvj420p or it errors
```

Then look at the frame. Confirm: text is a single centered line, clears the face, doesn't
collide with a hand or phone, and the subject is on-model.

## Style: the text look is fixed

`treatment: "outline"`, white fill, `fontScale: 0.85`, `font: "classic"` — the standard
TikTok text look. Do not substitute `highlight` (solid box), `bar`, or `shadow`, and do not
drift the scale. Consistency across a series is what makes the account read as one voice.

**When a background is too bright for outline text, move the text — never change the
treatment.** Measure the frame and put the caption in the darkest band that still clears
the face:

```py
from PIL import Image
g = Image.open("frame.jpg").convert("L")
w, h = g.size
for y in (0.16, 0.30, 0.56, 0.68, 0.72, 0.86):     # candidate anchors
    band = g.crop((0, int(h*y) - 40, w, int(h*y) + 40))
    px = list(band.getdata())
    print(y, sum(px) // len(px))                    # <100 is comfortable, >160 is bad
```

A dark t-shirt or tank at y≈0.68–0.72 typically measures 60–65 and is the most reliable
place to land text. **Text never covers the face** — above or below, never over.

## Step 4 — ship it

`schedule_post` publishes to the connected account (always pass `idempotencyKey`; for
TikTok pass `tiktokSettings` or it goes out private).

One honest caveat from the data behind this skill: on that account, versions **finished and
posted natively in the app** substantially out-performed the same video pushed through the
API — the winner did 1,861 views on Reels while the API-published twin of the same file
stalled. If reach matters more than automation, render with the agent and post the file by
hand. Compare on your own account before trusting either path.

Also: **Instagram Reels and TikTok are not interchangeable.** The same reaction video did
1,861 views on Reels and 283 on TikTok from comparable follower counts. Post both, then let
`get_engagement_summary` tell you which surface is yours.

## Writing the text

- **Quote the audience verbatim.** The cope goes in quotation marks in *their* words —
  "i'm 5'9 it's over for me" — so the target recognizes themselves in line one.
- **Reassure, don't motivate.** Second-person and calm ("nobody is measuring you bro")
  beats hype in this format. The character is a friend, not a coach.
- **Aim the post caption at an identity, not a topic.** The winner's caption was
  `You're okay lil man #relatable #shortking` — the niche identity hashtag found the exact
  people the on-screen text was written for.
- **Leave the door open for the next one.** An open-ended payoff invites the next cope in
  the comments, and each comment is a free script for the next video over the same base clip.

## Things this format cannot do

`create_video` type `green_screen_meme` **cannot use your character.** It requires a
`greenScreenId` from `list_green_screens`, a shared platform library, with no way to add
your own subject. For "character reacts to a comment", skip green screen entirely and use
this skill — the character's own footage with the comment quoted on screen reads more
native than a composite anyway.

## Checklist

- [ ] `whoami` → org resolved; `list_characters` → a `ready`, reference-consistent character
- [ ] Base clip prompt names the reaction beats, says mouth closed, forbids scene cuts
- [ ] Base clip frame-checked before any text goes on it
- [ ] Every beat: outline / white / 0.85 / classic, one line ≤30 chars (`\n` only for a
      deliberate two-line beat)
- [ ] Windows non-overlapping; payoff timed to the footage beat, not the clip midpoint
- [ ] Frame extracted and inspected at each window boundary
- [ ] Text clears the face and any hand/phone in frame
- [ ] Post caption names an identity; base clip saved for the next reply
