Step 1: design the character in ChatGPT
One prompt: a name, a body, clothes, one trait you can see, and 'shoot it like a real photo'. That last line is what makes it weird instead of cartoon.
Pick something with a silhouette you can read at thumbnail size, not a person. A croissant, a pigeon, a plant. Here is the exact prompt for Crumb, a bakery mascot, and what came back.
Design an original mascot for a neighborhood bakery's TikTok ads. His name is Crumb.
Crumb is a butter croissant the size of a house cat, standing upright on two skinny legs in white tube socks and red high-top sneakers. Two short stubby arms. Small round wire glasses. Deadpan, slightly tired expression, like he has worked the 5am shift for 30 years. Realistic flaky golden pastry texture with a few crumbs falling off.
Shoot it like a real photo, not a cartoon or 3D render: 50mm lens, soft daylight, shallow depth of field. Plain warm-grey studio background. Full body, centered, square image.



Every image on this page is the first result from the prompt shown, made with OpenAI's gpt-image-2.5 through the Images API. Paste the same prompt into ChatGPT and you get the same kind of result. The Gloria and Monty prompts are under their sheets below.
Step 2: lock it with a character reference sheet
Same chat, one follow-up: three views on one background. The sheet, not the original image, is what every video job gets from now on.
Turn this character into a character reference sheet I can reuse for video. Three views side by side on one plain warm-grey background, same lighting and same scale in every view:
1. Front view, head and upper body, looking straight at the camera.
2. Three-quarter view, full body, turned 45 degrees to the left.
3. Full body side profile, mid-stride, walking to the right.
Keep every detail identical to the image: shape, colours, textures, clothes, accessories and proportions. Photoreal, like the original. No text, no labels, no logos, no grid lines.


Design an original mascot for a budgeting app's TikTok and Reels ads. Her name is Gloria.
Gloria is a city pigeon the height of a person, standing upright like a human. Beige trench coat, belted. Pearl earrings. A tiny leather handbag hooked on one wing. Grey feathers with the green-purple sheen on her neck, orange eyes, one eyebrow-feather raised like she has just seen your card statement. Judgmental but classy.
Shoot it like a real photo, not a cartoon or 3D render: 50mm lens, soft daylight, shallow depth of field. Plain warm-grey studio background. Full body, centered, square image.

Design an original mascot for a houseplant delivery brand's TikTok and Reels ads. His name is Monty.
Monty is a monstera plant in a terracotta pot, standing on two long human-like legs in brown corduroy trousers and polished brown loafers. Big glossy split leaves on top, one leaf drooping over like a fringe. No face, no eyes: all the personality is in the posture. He stands with one hip cocked, slightly too confident.
Shoot it like a real photo, not a cartoon or 3D render: 50mm lens, soft daylight, shallow depth of field. Plain warm-grey studio background. Full body, centered, square image.
The call behind Crumb's sheet: the design image goes in, the sheet comes back in 26 seconds.

Step 3: have Seedance make the UGC base clip
Seedance refuses reference videos with real human faces, unless the clip came out of Seedance on your own account in the last 30 days. So the base clip is a Seedance clip, not phone footage.
Write the clip like a creator would shoot it: selfie angle, one line, one take. The person in it is a stand-in. Step 4 replaces them.
Vertical selfie UGC video. A woman in her 30s in a sunny kitchen holds a paper bakery bag up to the phone camera, takes a bite of a croissant and says: "Okay. This croissant is ridiculous." Handheld phone camera, natural light, one take, no music.
curl -s https://ark.ap-southeast.bytepluses.com/api/v3/contents/generations/tasks \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ARK_API_KEY" \
-d '{
"model": "dreamina-seedance-2-0-260128",
"content": [
{ "type": "text", "text": "Vertical selfie UGC video. A woman in her 30s in a sunny kitchen holds a paper bakery bag up to the phone camera, takes a bite of a croissant and says: \"Okay. This croissant is ridiculous.\" Handheld phone camera, natural light, one take, no music." }
],
"ratio": "9:16",
"resolution": "720p",
"duration": 8,
"generate_audio": true,
"watermark": false
}'
# -> { "id": "cgt-..." }Step 4: swap the character into the clip
Send the sheet as a reference image and the untouched base clip as a reference video, and tell Seedance who replaces whom. Motion, framing and the line stay; the person becomes Crumb.
curl -s https://ark.ap-southeast.bytepluses.com/api/v3/contents/generations/tasks \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ARK_API_KEY" \
-d '{
"model": "dreamina-seedance-2-0-260128",
"content": [
{ "type": "text", "text": "Replace the woman in Video 1 with the croissant character in Image 1. Keep the camera move, framing, timing and the line she says from Video 1. Keep the character identical to Image 1." },
{ "type": "image_url", "image_url": { "url": "https://your-cdn/crumb-sheet.jpg" }, "role": "reference_image" },
{ "type": "video_url", "video_url": { "url": "https://your-cdn/base-clip.mp4" }, "role": "reference_video" }
],
"ratio": "adaptive",
"duration": -1,
"resolution": "720p",
"generate_audio": true,
"watermark": false
}'
# Poll until "status" is "succeeded", then download content.video_url (it expires in 24 hours).
curl -s https://ark.ap-southeast.bytepluses.com/api/v3/contents/generations/tasks/$TASK_ID \
-H "Authorization: Bearer $ARK_API_KEY"const ARK = 'https://ark.ap-southeast.bytepluses.com/api/v3/contents/generations/tasks'
type Status = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'expired'
interface ArkTask {
id: string
status: Status
content?: { video_url?: string }
error?: { code: string; message: string } | null
}
const headers = () => {
const key = process.env.ARK_API_KEY
if (!key) throw new Error('ARK_API_KEY is not set')
return { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` }
}
/** Swap the character from a reference sheet into a Seedance base clip. Returns the task id. */
export async function createSwap(sheetUrl: string, baseClipUrl: string): Promise<string> {
const res = await fetch(ARK, {
method: 'POST',
headers: headers(),
body: JSON.stringify({
model: 'dreamina-seedance-2-0-260128',
content: [
{ type: 'text', text: "Replace the woman in Video 1 with the croissant character in Image 1. Keep the camera move, framing, timing and the line she says from Video 1. Keep the character identical to Image 1." },
{ type: 'image_url', image_url: { url: sheetUrl }, role: 'reference_image' },
{ type: 'video_url', video_url: { url: baseClipUrl }, role: 'reference_video' },
],
ratio: 'adaptive',
duration: -1,
resolution: '720p',
generate_audio: true,
watermark: false,
}),
})
if (!res.ok) throw new Error(`create failed ${res.status}: ${await res.text()}`)
const { id } = (await res.json()) as { id?: string }
if (!id) throw new Error('create returned no task id')
return id
}
/** Poll every 10 seconds for up to 15 minutes. */
export async function waitForVideo(id: string): Promise<string> {
const deadline = Date.now() + 15 * 60_000
while (Date.now() < deadline) {
const res = await fetch(`${ARK}/${id}`, { headers: headers() })
if (!res.ok) throw new Error(`poll failed ${res.status}: ${await res.text()}`)
const task = (await res.json()) as ArkTask
if (task.status === 'succeeded') {
// A success with no URL is still a failure.
if (!task.content?.video_url) throw new Error('succeeded without a video_url')
return task.content.video_url // expires in 24 hours: copy it to your own storage now
}
if (task.status !== 'queued' && task.status !== 'running') {
throw new Error(`task ${task.status}: ${task.error?.code ?? ''} ${task.error?.message ?? ''}`)
}
await new Promise((r) => setTimeout(r, 10_000))
}
throw new Error(`task ${id} timed out`)
}What comes back
The task returns one MP4 at the length of the base clip. The camera move, the framing, the timing and the line are the base clip's. The person is gone: Crumb holds the bakery bag up to the lens, takes the bite and says "Okay. This croissant is ridiculous." in the base clip's voice.
What to check before you keep it: the character matches the sheet from every angle the clip turns through, the hands or stubby arms hold the prop without melting into it, and the mouth roughly tracks the line. Reject and rerun the ones that drift; a new seed is cheaper than a fix in the edit.
Limits that bite
- Seedance 2.0 takes up to 9 reference images and 3 reference videos per task. Seedance 2.5 takes up to 30 images and 10 videos.
- Reference videos run 2 to 15 seconds on 2.0, MP4 or MOV, 200 MB or less. Images up to 30 MB, 300 to 6,000 px a side.
- Real human faces are refused as references. A clip Seedance made on your own account in the last 30 days, untouched, is the exception.
- The output URL lives for 24 hours. Copy the video to your own storage the moment the task succeeds.
- Turning Seedance 2.x on in BytePlus ModelArk takes $30 of credits or a resource pack. Individual accounts run 3 tasks at once.
What one variant costs
| Step | Tool | Cost |
|---|---|---|
| Character design | ChatGPT, one message | Included in ChatGPT |
| Reference sheet | ChatGPT, one follow-up | Included in ChatGPT |
| 8-second base clip, 720p, 9:16 | Seedance 2.0 at $7/M tokens | $1.21 |
| 8-second character swap, 720p | Seedance 2.0 with video input at $4.3/M tokens | $1.49 |
Step 5: cut the ad
The character carries the first two seconds. The product sits inside the bit, and the offer lands in the last two.
- The character on screen in the first two seconds, not saved for the end card.
- One bit per ad. Crumb is tired. Gloria judges. Monty poses.
- The product inside the bit: Crumb is the croissant she is eating.
- Burned-in captions, 9:16, the offer in the last two seconds.
- The same reference sheet every week, so week ten looks like week one.
Director by Sprites · private beta
Keep the pipeline full. Every Monday.
Director is a full creative team for your Meta ads. Every Monday you get finished, on-brand video ads, reviewed by a human. You pick the winners. Sprites ships the campaigns.
- Strategy, hooks, scripts, shooting and editing, end to end
- New concepts before the current winner fades
- No prompting, no endless revisions
AI mascot: questions
What is an AI mascot?
An AI mascot is a brand character generated with AI and reused across every ad, like a Duolingo owl you can make in an afternoon. It works because it is the same character every time, so people start to recognise it.
How do I make a consistent AI character?
Design the character once in ChatGPT, then ask for a reference sheet with a front, three-quarter and full-body view on one plain background. Give that same sheet to your video model as the reference on every job.
Can ChatGPT make a character reference sheet?
Yes. Generate the character, then in the same chat ask it to turn the character into a reference sheet with three views on one background, keeping every detail identical. The sheet prompt on this page is the one that made all three sheets shown.
How do I use Seedance for an AI character video?
Send the reference sheet as a reference_image and a short clip as a reference_video to the Seedance 2.0 task endpoint, with a prompt that says which person to replace with which character. Poll the task until it succeeds and download the video within 24 hours.
Why does Seedance reject my UGC video?
Seedance 2.0 refuses reference images and videos that contain real human faces. Generate the base clip in Seedance on your own account first, then use that untouched clip as the reference video within 30 days.
How much does an AI character ad cost to make?
The character and its sheet cost one ChatGPT conversation. On BytePlus list prices an 8-second 720p base clip is about $1.21 and the character swap about $1.49, so under $3 of video per variant before editing.
Want a weekly pipeline of these finished?
Director delivers finished, human-reviewed video ads every Monday. You pick the winners. Sprites ships the campaigns.