---
name: eggapi-ugc-video-ad
description: Make UGC-style talking video ads with EggAPI - several scripts with different hooks, one consistent AI presenter, clips where the presenter speaks the lines with generated voice, then vertical cuts with burned-in captions via ffmpeg. Use when the user wants a UGC ad, talking-head or testimonial-style video, creator-style product review, spokesperson video or short-form video ad for TikTok, Reels, Shorts, Douyin or Kuaishou (UGC 广告、口播视频、达人带货视频、真人出镜广告、种草视频).
---

# UGC talking video ads

Make short vertical ads where one presenter talks to the camera about a product, in several versions that differ only in the opening hook, so the user can test which hook wins. Reply in the user's language. Write model prompts in English, but write the spoken lines and captions in the ad's language.

## EggAPI basics

This skill uses the EggAPI MCP server, usually added under the name `eggapi`: `list_models`, `generate_image`, `generate_video` and `get_task` (in Claude Code they show up as `mcp__eggapi__*`). If these tools are missing, stop and send the user to https://eggapi.ai/connect to add the server.

- **Models.** Call `list_models` once at the start. Use the model IDs this skill names when they are listed; otherwise pick the closest model with the same `generation_type`. `unit_price_usd` is the price per image, or per second of video; `unit_price_by_resolution` overrides it for a given `resolution`.
- **Local files.** The generate tools only accept public `http(s)` URLs. Upload local files first (up to 20 MB per image):

  ```bash
  curl -sS https://api.eggapi.ai/v1/uploads \
    -H "Authorization: Bearer $EGGAPI_API_KEY" \
    -F "files[]=@./photo-1.jpg" -F "files[]=@./photo-2.jpg"
  ```

  The URLs are in `data.files[].url`. If `EGGAPI_API_KEY` is empty in the shell, ask the user to export it; never ask for the key in the chat. Uploaded files can be opened by anyone who has the URL, so say so before uploading anything private.
- **Tasks are async.** `generate_image` and `generate_video` return a `task_id` at once. Submit every independent task first, then poll `get_task` for each until `done` is true: every 5-10 s for images, 10-15 s for videos (usually 1-3 min). If a generate call already returns `urls`, no polling is needed.
- **Always pass** `aspect_ratio` (the default is 16:9) and, for video, `duration` (4-15 s). Supported ratios: `1:1`, `3:4`, `4:3`, `9:16`, `16:9`, `21:9`.
- **Edit models return one image per call.** For more variants, call again; `num_outputs` only applies to text-to-image models.
- **Output URLs work as inputs.** Pass a finished image's URL straight into the next edit or video call; no re-upload needed.
- **Save everything.** Download each output as soon as its task completes, into `eggapi-output/<project>/` under the current directory, with `curl -sSL -o <file> <url>`: some output links expire after 24 hours, and some file hosts reject other HTTP clients. Keep the extension from the URL and use descriptive file names.
- **Cost.** Show the plan with an estimated cost before generating and wait for a go-ahead, unless the user already said to go ahead. At the end, report each task's `cost` and the total.
- **Errors.** `insufficient balance`: ask the user to top up at https://eggapi.ai/dashboard. `temporarily unavailable`: switch to another model of the same type. A `failed` task: read `error`, adjust the prompt if it points at the content, and retry once.

## Ground rules

- The presenter is AI-generated. Do not script made-up personal results ("I lost 10 kg in a month") or fake reviews; keep claims to facts about the product that the user can back up. Remind the user to switch on the platform's label for AI-generated content when they upload.
- Never imitate a real creator, celebrity or other identifiable person. A user's own photo of a presenter needs that person's permission.

## 1. Brief

Ask only for what is missing:

- **Product**, product photos, and 3-5 facts or benefits the user can stand behind.
- **Audience** and **platform**. Default format `9:16`.
- **Length**: default 20 seconds; 15-30 seconds works for most placements.
- **Language** and the presenter's tone (excited, calm, expert, funny).
- **Presenter**: a description (age range, style, setting), or the user's own photo.
- **Setting**: bedroom, kitchen, car, bathroom mirror, gym, office.
- **Hooks**: how many versions to test (default 3).

## 2. Write the scripts and confirm

Use one body and several hooks, so every version shares most of the footage and only the hook is rendered per version:

- **Hook** (one clip, 4-6 s): a question, a bold claim the product backs up, a relatable problem, or a surprising demo. One line per hook.
- **Body** (one or two clips, up to 15 s each): the problem, the product and how it solves it, one or two facts, then the call to action.

Spoken pace is about 2.5 words per second in English and about 4 characters per second in Chinese, so a 12-second clip holds about 30 English words. Set each clip's duration about one second longer than its lines take to say: the models leave a short pause before and after the speech, which is trimmed in step 5. Write lines as short phrases separated by commas; each phrase becomes one caption.

Show a table: clip, duration, spoken line, what we see (selfie talking, product close-up, demo). Estimate the cost: total seconds of all clips x the video price per second at the chosen resolution, plus the presenter images. Wait for the go-ahead.

## 3. Presenter frames

1. **Presenter**: `generate_image` with `google/nano-banana-pro/text-to-image`, `aspect_ratio: "9:16"`, `resolution: "2k"`, `num_outputs: 3`:

   > Smartphone selfie photo of {presenter}, holding the phone at arm's length, looking into the camera, natural window light, {setting}, casual and authentic, slightly imperfect framing, no beauty filter.

   Let the user pick one. A user's own photo replaces this step.

2. **First frames**: for each clip, make its opening frame with `google/nano-banana-pro/edit` (alternative `bytedance/seedream-v5.0-pro/edit`), passing the presenter image and the product photos:

   > The person from the first image, exactly the same face, hair and clothes, {holding the product up to the camera / pointing at the product on the table}. The product looks exactly as in the other images: same shape, colors, logo and label text. Same selfie framing, same {setting}, same light.

## 4. Render the clips

Render the first hook as a test before anything else:

- `generate_video` with `bytedance/seedance-2.5/image-to-video`, which gave the most natural speech in testing. `alibaba/wan-3.0/image-to-video` costs half as much per second or less; offer it when the budget is tight, and render its test clip too. Use the same model for every clip.
- `image`: the clip's first frame. `aspect_ratio`: `9:16`. `duration`: the clip's length. `resolution`: `720p` for the test, `1080p` for the final.
- `parameters: {"generate_audio": true}`.
- Prompt the delivery and the exact line:

  > The woman looks into the camera and starts talking right away, saying in {language}, in a {tone} voice: "{line}". Handheld selfie video with slight natural shake, she gestures with the product while talking. No background music. No on-screen text.

Check the test with the user: is the speech clear, in the right language, in sync with the lips, and is the voice right? Voices can differ from clip to clip, so keep each version's talking to as few clips as possible. If speech is not good enough, offer the fallback: render the clips without lines (`generate_audio: false`, prompt the gestures and the demo only) and let the user record a voiceover.

Once the test is approved, submit the other hooks and the body clips together. Product close-ups or demos without speech can be made from a product photo in the same way, with the motion only in the prompt.

## 5. Captions and assembly

Download the clips as `hook-a.mp4`, `hook-b.mp4`, `body-1.mp4` and so on. The steps below need `ffmpeg`.

**Trim the silence at the start of each clip**, so a hook speaks in its first second. This prints how far to cut, keeping 0.15 s before the first sound, or nothing when the clip already starts with sound:

```bash
cd eggapi-output/<campaign-slug>
lead=$(ffmpeg -hide_banner -nostats -i hook-a.mp4 -af silencedetect=noise=-35dB:d=0.3 -vn -f null - 2>&1 \
  | awk '/silence_start/ {s=$NF} /silence_end/ {if (s+0 < 0.05 && $5 > 0.2) printf "%.2f", $5 - 0.15; exit}')
if [ -n "$lead" ]; then
  ffmpeg -ss "$lead" -i hook-a.mp4 -c:v libx264 -crf 18 -pix_fmt yuv420p -c:a aac hook-a-trim.mp4
else
  cp hook-a.mp4 hook-a-trim.mp4
fi
```

**Join each version and even out the loudness**; clips can differ by several decibels and peak at full scale:

```bash
printf "file '%s'\n" hook-a-trim.mp4 body-1-trim.mp4 > version-a.txt
ffmpeg -f concat -safe 0 -i version-a.txt -af loudnorm=I=-14:TP=-1.5:LRA=11 -ar 48000 \
  -c:v libx264 -crf 18 -pix_fmt yuv420p -c:a aac version-a-raw.mp4
```

**Time the captions.** One caption per phrase, without closing punctuation, so each fits on one line. Run the `silencedetect` command above on the joined video to see where each clip's speech starts and ends, then split that span across the clip's phrases in proportion to their length. Write the timings to `version-a.srt`.

Then burn in the captions. Many ffmpeg builds, including Homebrew's, ship without the `subtitles` filter, so check first with `ffmpeg -hide_banner -filters | grep -w subtitles`.

- **With the `subtitles` filter**:

  ```bash
  ffmpeg -i version-a-raw.mp4 -vf "subtitles=version-a.srt:force_style='Fontsize=13,Bold=1,Outline=2,Alignment=2,MarginV=80'" -c:a copy version-a.mp4
  ```

  For Chinese, Japanese or Korean add `FontName=` with a font that has those characters, such as `Hiragino Sans GB` or `Apple SD Gothic Neo` on macOS.

- **Without it, using ImageMagick**: render each caption as an image about 80% of the frame wide, with the font size at about a 18th of the frame width, then overlay each one during its time range with ffmpeg's built-in `overlay` filter:

  ```bash
  FONT="/System/Library/Fonts/Hiragino Sans GB.ttc"
  W=$(ffprobe -v error -select_streams v -show_entries stream=width -of csv=p=0 version-a-raw.mp4)
  magick -background none -fill white -font "$FONT" -pointsize $((W / 18)) -size $((W * 8 / 10))x -gravity center caption:"{phrase 1}" \
    -extent '%[fx:w+28]x%[fx:h+20]' -background '#000000A0' -flatten cap-1.png
  # ...one image per phrase, then:
  ffmpeg -i version-a-raw.mp4 -i cap-1.png -i cap-2.png -filter_complex \
    "[0:v][1:v]overlay=(W-w)/2:H*0.62:enable='between(t,0,3)'[v1];[v1][2:v]overlay=(W-w)/2:H*0.62:enable='between(t,3,7)'[v]" \
    -map "[v]" -map 0:a -c:v libx264 -crf 18 -pix_fmt yuv420p -c:a copy version-a.mp4
  ```

  Pick a font file that has the caption's characters: on macOS `Hiragino Sans GB.ttc` for Chinese, `AppleSDGothicNeo.ttc` for Korean and `Helvetica.ttc` for Latin scripts, all in `/System/Library/Fonts/`; on Linux find one with `fc-list :lang=zh`.

Either way the captions sit above the bottom quarter of the vertical frame, where the app's buttons cover it. Grab a frame to confirm: `ffmpeg -ss 2 -i version-a.mp4 -frames:v 1 check.png`.

If neither works, deliver the uncaptioned cut with its `.srt` file, which TikTok, CapCut and most editors can import. Without ffmpeg, deliver the clips and the `.srt` files, and tell the user how to install it (`brew install ffmpeg` on macOS, `sudo apt install ffmpeg` on Debian or Ubuntu).

## 6. Deliver

Save to `eggapi-output/<campaign-slug>/`: the presenter image, the first frames, every clip, `version-a.mp4`, `version-b.mp4` and so on, their `.srt` files, and `manifest.csv` with the columns `file,version,hook,duration,model,cost`. Finish with the list of versions and the total cost, and remind the user to label the ads as AI-generated where the platform asks.
