Avatar Camera Angles
Turn one still of a person talking to camera into a set of realistic
camera-angle variations of the same recording — same face, same outfit,
same room, same light, only the virtual camera moves. Built for vertical reels
where the shot cuts every 4–8 seconds while the speaker keeps talking to the
lens (the exact pattern seen in real talking-head reels).
This skill is a thin wrapper around the gpt-image-2 skill. gpt-image-2
preserves a reference image's identity at high fidelity; this skill adds the
prompt engineering that turns that into a believable camera move instead of a
new portrait.
Why it works (validated recipe)
Each prompt is assembled from three parts:
- Fixed identity/scene block — locks the subject, wardrobe, background
props, and lighting so the result reads as another frame of the same video.
- Camera slot — the single thing that changes, from the move catalog.
- Framing anchor — stops the model's main failure mode: drifting wider /
looser than the source. Most moves keep a tight chest-up frame;
pull_out
and the negative_space_* moves override the anchor on purpose.
This was validated empirically against a real talking-head frame: identity and
the room stay consistent across moves while only the camera position changes.
Setup
Generation runs through gpt-image-2, which owns the Replicate token (shared
across the Replicate skills — no separate key needed). This skill only needs
Pillow for the optional 9:16 crop:
pip3 install -r ~/.cursor/skills/avatar-camera-angles/scripts/requirements.txt
# gpt-image-2 must be installed too (usually already is):
pip3 install -r ~/.cursor/skills/gpt-image-2/scripts/requirements.txt
Workflow
- Pick the reference frame. A sharp, front-ish talking-head still of the
avatar (e.g. an
avatar-frames output). 1 ref is enough; up to 3 helps lock
identity.
- Write the scene profile. Look at the frame and describe four fields —
subject, wardrobe, scene, light — in a small JSON file (see
examples/lolo_scene.json). This is the most
important step: the more accurate it is, the less the scene drifts. You can
also pass the fields as --subject/--wardrobe/--scene/--light.
- Choose camera moves from the catalog (
--list).
- Generate the masters, optionally with
--crop916 for the reel-ready 9:16
crop (master 2:3) or --crop169 for a 16:9 YouTube crop (master 3:2).
- Review & re-roll. Use
--count N or re-run a move to pick the best take.
- Animate each still with lip-sync per shot (the
seedance-2 skill or
VEED Fabric) and stitch the clips into the reel, cutting between angles every
4–8s.
Quick reference
# Inspect the catalog (no API call)
python3 ~/.cursor/skills/avatar-camera-angles/scripts/generate_angles.py --list
# Preview an assembled prompt without generating
python3 ~/.cursor/skills/avatar-camera-angles/scripts/generate_angles.py \
--scene-file scene.json --move dutch_tilt --print-prompt
# Generate a few angles (masters in 2:3) + reel-ready 9:16 crops
python3 ~/.cursor/skills/avatar-camera-angles/scripts/generate_angles.py \
--ref frame_0001.png --scene-file scene.json \
--move push_in --move low_angle --move three_quarter --move negative_space_left \
--crop916 -o out/ --slug lolo
# Generate only the empirically validated moves
python3 ~/.cursor/skills/avatar-camera-angles/scripts/generate_angles.py \
--ref frame_0001.png --scene-file scene.json --validated-only --crop916 -o out/
# 16:9 landscape crops for YouTube (masters in 3:2, cropped to _169.png)
python3 ~/.cursor/skills/avatar-camera-angles/scripts/generate_angles.py \
--ref frame_0001.png --scene-file scene.json \
--move push_in --move three_quarter --crop169 -o out/ --slug lolo
Every run prints a JSON object to stdout with a results array (each item has
master and, with --crop916/--crop169, reel_916/reel_169). Per-move prompts are saved next to
the images as <slug>_<move>.prompt.txt for transparency.
Camera-move catalog
| Move |
Validated |
Use it for |
push_in |
✓ |
Tighter close-up; emphasis / intimacy. The safest, cleanest cut. |
pull_out |
✓ |
Wider medium shot (waist + desk); a visual "breather" / establishing beat. |
low_angle |
✓ |
Subtle contrapicado; a touch more authority. |
high_angle |
✓ |
Soft picado; intimate, confessional. |
three_quarter |
✓ |
Camera to frame-right (subject's left); dynamic, cinematic. |
three_quarter_mirror |
– |
Same, mirrored to the other side. |
profile |
– |
Strong near-profile (~50°); experimental, can drift more. |
dutch_tilt |
✓ |
Canted horizon ~9°; editorial energy. |
negative_space_left |
✓ |
Subject on the right, clean empty space on the LEFT for captions. |
negative_space_right |
– |
Subject on the left, clean empty space on the RIGHT for captions. |
pip |
– |
Centered, locked close-up for a circular picture-in-picture badge (e.g. avatar over a broll-web-capture base). Tight, even margin all around, eye-level, no rotation — meant to be lip-synced with a locked camera (no push/zoom). |
"Validated" = verified to keep identity + scene consistent on a real frame.
The others are sound variations; preview/re-roll them as needed.
The pip move (picture-in-picture badge)
When an avatar appears as a small circular badge over a base layer (a
broll-web-capture capture, a demo, a terminal), it should be a dedicated
shot, not a reused angle: a tight, perfectly centered face close-up with
even margin all around (so the circle never clips the face or hair), framed at
eye level with no rotation.
- Generate it at
1:1 so the circular crop wastes nothing:
--move pip -ar 1:1 (skip --crop916 — the square master is the PiP source).
- Keep it still when you animate it. This still drives a lip-synced clip,
but the PiP face must stay put. For the PiP, always lip-sync with
avatar-talking-video (p-video-avatar),
not seedance-2, keeping the camera locked: pass a --video-prompt
like "The person is talking, head still, no camera movement" (no push-in,
pull-out, zoom or dolly) so only the face moves. The base layer carries the
motion; the avatar is the steady credential anchor.
- Don't burn subtitles into the PiP clip — captions go on the whole reel
frame (the reel composer's finish pass), not inside the circle.
Aspect ratio: master 2:3 → 9:16, or master 3:2 → 16:9
gpt-image-2 renders natively only at 1:1 / 3:2 / 2:3. For 9:16 reels this
skill generates the master at 2:3 (the cleanest vertical — no canvas
padding, full native resolution 1024×1536) and --crop916 center-crops the
sides to a 9:16 reel frame (864×1536). For 16:9 YouTube pass --crop169:
the master defaults to 3:2 and is center-cropped to a 16:9 frame (~1536×864).
Both keep native resolution (no upscaling); keep the masters as the archive and
feed the crops (_916.png / _169.png) to the reel.
Options
| Option |
Default |
Description |
--ref PATH |
— |
Avatar reference frame (repeatable, 1–3). Required to generate. |
--scene-file |
— |
JSON profile: subject, wardrobe, scene, light. |
--subject/--wardrobe/--scene/--light |
— |
Per-field overrides (or use instead of a file). |
--move NAME |
— |
Camera move (repeatable). |
--all / --validated-only |
— |
Run the whole catalog / only validated moves. |
--crop916 |
off |
Also write a 9:16 center-crop per image (_916.png). |
--crop169 |
off |
Also write a 16:9 landscape crop per image (_169.png, YouTube). |
--output, -o |
. |
Output directory. |
--slug |
angle |
Output filename prefix. |
--aspect-ratio, -ar |
2:3 (or 3:2 with --crop169) |
Master ratio passed to gpt-image-2. |
--quality, -q |
high |
Fidelity (low/medium/high/auto). |
--count, -n |
1 |
Variations per move (1–10). |
--retries |
2 |
Retries per generation on transient Replicate timeouts. |
--list / --print-prompt |
— |
Inspect catalog / preview prompts (no API call). |
Tips
- Consistency across a reel: always use the same reference frame (and the
same scene profile) for every shot, so the avatar doesn't drift between cuts.
- Subtle beats subtle: small camera moves (
push_in, low_angle,
high_angle) feel like real edits; big ones (profile, hard three_quarter)
are punchier but drift more — preview before committing.
- Captions: generate
negative_space_* shots for any segment that needs an
on-screen headline; the empty side is left clean for text.
- Background variation is fine: the model reconstructs occluded props
slightly differently per angle — that actually reads as natural across cuts.
- Faster drafts: use
--quality low to scout framings, then re-run the
keepers at high.
Related skills
See REFERENCE.md for the prompt internals and design notes.
1---2name: avatar-camera-angles3description: Generate realistic camera-angle variations of a talking-head avatar from a single reference frame, reusing the gpt-image-2 skill. Produces the SAME person, outfit, room and lighting seen from a different virtual camera position (push-in, pull-out, low/high angle, three-quarter, Dutch tilt, off-center negative space for captions) so a reel can "cut" the camera every 4-8s while the speaker keeps addressing the lens. Each still then drives a lip-synced clip (seedance-2 / VEED Fabric). Use when the user wants to create reel shots / camera cuts / multiple angles / perspectives for an avatar or talking-head video, simulate a multi-cam talking head, or vary the framing of a presenter while keeping identity and scene consistent.4---56# Avatar Camera Angles78Turn **one** still of a person talking to camera into a set of **realistic9camera-angle variations** of the *same* recording — same face, same outfit,10same room, same light, only the virtual camera moves. Built for vertical reels11where the shot cuts every 4–8 seconds while the speaker keeps talking to the12lens (the exact pattern seen in real talking-head reels).1314This skill is a **thin wrapper around the `gpt-image-2` skill**. gpt-image-215preserves a reference image's identity at high fidelity; this skill adds the16prompt engineering that turns that into a believable camera move instead of a17new portrait.1819## Why it works (validated recipe)2021Each prompt is assembled from three parts:22231. **Fixed identity/scene block** — locks the subject, wardrobe, background24 props, and lighting so the result reads as another frame of the same video.252. **Camera slot** — the single thing that changes, from the move catalog.263. **Framing anchor** — stops the model's main failure mode: drifting wider /27 looser than the source. Most moves keep a tight chest-up frame; `pull_out`28 and the `negative_space_*` moves override the anchor on purpose.2930This was validated empirically against a real talking-head frame: identity and31the room stay consistent across moves while only the camera position changes.3233## Setup3435Generation runs through `gpt-image-2`, which owns the Replicate token (shared36across the Replicate skills — no separate key needed). This skill only needs37Pillow for the optional 9:16 crop:3839```bash40pip3 install -r ~/.cursor/skills/avatar-camera-angles/scripts/requirements.txt41# gpt-image-2 must be installed too (usually already is):42pip3 install -r ~/.cursor/skills/gpt-image-2/scripts/requirements.txt43```4445## Workflow46471. **Pick the reference frame.** A sharp, front-ish talking-head still of the48 avatar (e.g. an `avatar-frames` output). 1 ref is enough; up to 3 helps lock49 identity.502. **Write the scene profile.** Look at the frame and describe four fields —51 `subject`, `wardrobe`, `scene`, `light` — in a small JSON file (see52 [examples/lolo_scene.json](examples/lolo_scene.json)). This is the most53 important step: the more accurate it is, the less the scene drifts. You can54 also pass the fields as `--subject/--wardrobe/--scene/--light`.553. **Choose camera moves** from the catalog (`--list`).564. **Generate** the masters, optionally with `--crop916` for the reel-ready 9:1657 crop (master `2:3`) or `--crop169` for a 16:9 YouTube crop (master `3:2`).585. **Review & re-roll.** Use `--count N` or re-run a move to pick the best take.596. **Animate each still** with lip-sync per shot (the **`seedance-2`** skill or60 VEED Fabric) and stitch the clips into the reel, cutting between angles every61 4–8s.6263## Quick reference6465```bash66# Inspect the catalog (no API call)67python3 ~/.cursor/skills/avatar-camera-angles/scripts/generate_angles.py --list6869# Preview an assembled prompt without generating70python3 ~/.cursor/skills/avatar-camera-angles/scripts/generate_angles.py \71 --scene-file scene.json --move dutch_tilt --print-prompt7273# Generate a few angles (masters in 2:3) + reel-ready 9:16 crops74python3 ~/.cursor/skills/avatar-camera-angles/scripts/generate_angles.py \75 --ref frame_0001.png --scene-file scene.json \76 --move push_in --move low_angle --move three_quarter --move negative_space_left \77 --crop916 -o out/ --slug lolo7879# Generate only the empirically validated moves80python3 ~/.cursor/skills/avatar-camera-angles/scripts/generate_angles.py \81 --ref frame_0001.png --scene-file scene.json --validated-only --crop916 -o out/8283# 16:9 landscape crops for YouTube (masters in 3:2, cropped to _169.png)84python3 ~/.cursor/skills/avatar-camera-angles/scripts/generate_angles.py \85 --ref frame_0001.png --scene-file scene.json \86 --move push_in --move three_quarter --crop169 -o out/ --slug lolo87```8889Every run prints a JSON object to stdout with a `results` array (each item has90`master` and, with `--crop916`/`--crop169`, `reel_916`/`reel_169`). Per-move prompts are saved next to91the images as `<slug>_<move>.prompt.txt` for transparency.9293## Camera-move catalog9495| Move | Validated | Use it for |96|---|---|---|97| `push_in` | ✓ | Tighter close-up; emphasis / intimacy. The safest, cleanest cut. |98| `pull_out` | ✓ | Wider medium shot (waist + desk); a visual "breather" / establishing beat. |99| `low_angle` | ✓ | Subtle contrapicado; a touch more authority. |100| `high_angle` | ✓ | Soft picado; intimate, confessional. |101| `three_quarter` | ✓ | Camera to frame-right (subject's left); dynamic, cinematic. |102| `three_quarter_mirror` | – | Same, mirrored to the other side. |103| `profile` | – | Strong near-profile (~50°); experimental, can drift more. |104| `dutch_tilt` | ✓ | Canted horizon ~9°; editorial energy. |105| `negative_space_left` | ✓ | Subject on the right, clean empty space on the LEFT for captions. |106| `negative_space_right` | – | Subject on the left, clean empty space on the RIGHT for captions. |107| `pip` | – | **Centered, locked close-up for a circular picture-in-picture badge** (e.g. avatar over a `broll-web-capture` base). Tight, even margin all around, eye-level, no rotation — meant to be lip-synced with a **locked camera** (no push/zoom). |108109"Validated" = verified to keep identity + scene consistent on a real frame.110The others are sound variations; preview/re-roll them as needed.111112### The `pip` move (picture-in-picture badge)113114When an avatar appears as a small circular badge over a base layer (a115`broll-web-capture` capture, a demo, a terminal), it should be a **dedicated116shot**, not a reused angle: a tight, perfectly **centered** face close-up with117even margin all around (so the circle never clips the face or hair), framed at118eye level with no rotation.119120- **Generate it at `1:1`** so the circular crop wastes nothing:121 `--move pip -ar 1:1` (skip `--crop916` — the square master *is* the PiP source).122- **Keep it still when you animate it.** This still drives a lip-synced clip,123 but the PiP face must **stay put**. For the PiP, **always lip-sync with124 [`avatar-talking-video`](../avatar-talking-video/SKILL.md) (`p-video-avatar`),125 not `seedance-2`**, keeping the camera **locked**: pass a `--video-prompt`126 like *"The person is talking, head still, no camera movement"* (no push-in,127 pull-out, zoom or dolly) so only the face moves. The base layer carries the128 motion; the avatar is the steady credential anchor.129- Don't burn subtitles into the PiP clip — captions go on the **whole reel130 frame** (the reel composer's finish pass), not inside the circle.131132## Aspect ratio: master 2:3 → 9:16, or master 3:2 → 16:9133134gpt-image-2 renders natively only at `1:1 / 3:2 / 2:3`. For **9:16 reels** this135skill generates the **master at `2:3`** (the cleanest vertical — no canvas136padding, full native resolution ~1024×1536) and `--crop916` **center-crops** the137sides to a `9:16` reel frame (~864×1536). For **16:9 YouTube** pass `--crop169`:138the master defaults to `3:2` and is center-cropped to a `16:9` frame (~1536×864).139Both keep native resolution (no upscaling); keep the masters as the archive and140feed the crops (`_916.png` / `_169.png`) to the reel.141142## Options143144| Option | Default | Description |145|---|---|---|146| `--ref PATH` | — | Avatar reference frame (repeatable, 1–3). Required to generate. |147| `--scene-file` | — | JSON profile: `subject`, `wardrobe`, `scene`, `light`. |148| `--subject/--wardrobe/--scene/--light` | — | Per-field overrides (or use instead of a file). |149| `--move NAME` | — | Camera move (repeatable). |150| `--all` / `--validated-only` | — | Run the whole catalog / only validated moves. |151| `--crop916` | off | Also write a 9:16 center-crop per image (`_916.png`). |152| `--crop169` | off | Also write a 16:9 landscape crop per image (`_169.png`, YouTube). |153| `--output, -o` | `.` | Output directory. |154| `--slug` | `angle` | Output filename prefix. |155| `--aspect-ratio, -ar` | `2:3` (or `3:2` with `--crop169`) | Master ratio passed to gpt-image-2. |156| `--quality, -q` | `high` | Fidelity (`low/medium/high/auto`). |157| `--count, -n` | `1` | Variations per move (1–10). |158| `--retries` | `2` | Retries per generation on transient Replicate timeouts. |159| `--list` / `--print-prompt` | — | Inspect catalog / preview prompts (no API call). |160161## Tips162163- **Consistency across a reel:** always use the *same* reference frame (and the164 same scene profile) for every shot, so the avatar doesn't drift between cuts.165- **Subtle beats subtle:** small camera moves (`push_in`, `low_angle`,166 `high_angle`) feel like real edits; big ones (`profile`, hard `three_quarter`)167 are punchier but drift more — preview before committing.168- **Captions:** generate `negative_space_*` shots for any segment that needs an169 on-screen headline; the empty side is left clean for text.170- **Background variation is fine:** the model reconstructs occluded props171 slightly differently per angle — that actually reads as natural across cuts.172- **Faster drafts:** use `--quality low` to scout framings, then re-run the173 keepers at `high`.174175## Related skills176177- [`gpt-image-2`](../gpt-image-2/SKILL.md) — the underlying image generator.178- [`avatar-frames`](../avatar-frames/SKILL.md) — extract clean reference frames.179- [`seedance-2`](../seedance-2/SKILL.md) — animate each still (lip-sync / motion).180- [`avatar-video-reel`](../avatar-video-reel/SKILL.md) — full reel pipeline.181182See [REFERENCE.md](REFERENCE.md) for the prompt internals and design notes.