Chapter 4 of 9. Prerequisite: Chapter 3. You have a CLI wrapper that assembles role-tagged content arrays and submits them in parallel. This chapter gives it the one input that survives dozens of independent renders: a registered character.
Character drift is the complaint that kills most AI short films. Shot 1 and shot 7 are the same person in the script and two strangers on screen. The fix is an asset system that BytePlus ships and almost nobody writes about: register the face once, reference it by id forever. This chapter registers a character, proves the lock with a single 480p render, and banks the moderation rule that makes registration non-optional.
The drift problem: two ways an unregistered face fails
An unregistered character fails in two distinct places, and only one of them is obvious.
The obvious failure is drift. A prompt-described character ("a woman with silver hair and a red jacket") is re-invented on every render. Across one clip the model holds her. Across eight independent renders submitted in parallel, the jawline changes, the hair changes, the age changes. There is no memory between tasks; the create-task API is stateless by design, so consistency has to arrive inside the request.
The non-obvious failure is moderation. The intuitive fix for drift is to paste the character's face image into every request as a raw upload. On this pipeline that produced a hard block. Uploading the protagonist's own face PNG as first and last frame returned:
HTTP 400 InputImageSensitiveContentDetected.PrivacyInformation
with the message that the input image may contain a real person. The face is AI-generated. The moderation layer cannot know that, so it refuses the raw upload. The same face, referenced through its registered asset:// id, sailed through with no privacy block at all.
That asymmetry is the whole argument for this chapter. Asset trust and raw-upload trust are separate systems. Registration moves the trust decision out of the per-request moderation path and into a one-time review step. Once the platform has warehoused your character, every subsequent render treats it as a known quantity.
Seedance character consistency: how asset registration works
Seedance 2.0 accepts four input modalities: text, image, audio, and video, and its reference-media system lets an image in the content array act as an identity anchor rather than a frame. The asset system sits one level above that: instead of inlining the identity image as base64 on every request, you register it once in the ModelArk console as a Virtual Portrait Asset and reference it by id.
The id you get back has the shape asset://asset-<timestamp>-<suffix>. In every request from then on, asset://<your-asset-id> goes wherever an image_url would go. The video generation tutorial covers the general multimodal request assembly; the asset id is a drop-in URL scheme inside it.
Two decisions before you touch the console.
One clean front view, never a model sheet. The instinct from game art is to register a 4-view turnaround. Resist it. A single clean front-view crop gives the best face-lock; a 4-view sheet confuses identity, because the model averages the views. This is validated, not theorised: a front-crop-only asset held the character consistent through a full turn away from camera and back, on both characters in this course's practice film, with the model sheet never registered at all.
Stay on the free tier. Virtual Portrait Assets live under Advanced Creation Rights, which is tiered: Entry is free and holds 50 assets, Standard is $1,400/mo for 1M assets, Premium is $4,200/mo. A short film needs single-digit assets. Fifty is plenty.
Registering the asset: eight steps in the console
The console flow, in order. Budget ten minutes.
- Open Advanced Creation Rights
In the ModelArk console (the same console where you obtained your API key in Chapter 1), go to Model activation, then Advanced Creation Rights.
- Pick Virtual Portrait Asset
Select Virtual Portrait Asset. Do not select Portrait Assets; that is the real-human authorisation flow from the warning above.
- Create a new asset group
Click New asset group. Groups are how you keep a cast together; one group per project works.
- Prepare the image
Produce a single clean front-view crop of the character. Face fully visible, neutral expression, no other faces in frame. Skip the turnaround sheet entirely.
- Encode the metadata in the filename
The upload filename is parsed as metadata using the exact convention
Title && Group && Description. Each segment may contain only[A-Za-z0-9_-]. Spaces throw an error, and the parsed fields are not editable after upload, so a wrong filename means deleting and re-uploading.Ellie && Ellie_Cast && front_pose_crop.pngis the shape. - Upload through the native picker
The upload control is a native file picker only. Browser-automation tools cannot hand it a host path, so this one step is always manual even in an otherwise agent-driven pipeline.
- Wait for status warehoused
The asset goes through review. When its status reads warehoused, it is trusted and renderable. Do not reference it before then.
- Copy the asset id
Copy the
asset://asset-<timestamp>-<suffix>id from the console entry. This string is the character from now on.
The face-lock validation: one render, thirty-five cents
Never take a fresh asset into production unproven. The validation is one deliberately cheap render: 480p, 9:16, five seconds, with a prompt that forces the hardest identity test a single shot can hold, a turn away from camera and back. If the face that comes back after the turn is the same face, the lock is real.
The request, submitted to the create-task endpoint exactly as Chapter 3 built it:
{
"model": "dreamina-seedance-2-0-260128",
"content": [
{
"type": "text",
"text": "@Image1 stands centre frame in a plain studio, turns away from camera, then turns back to face front. Soft even lighting, static camera."
},
{
"type": "image_url",
"image_url": { "url": "asset://<your-asset-id>" },
"role": "reference_image"
}
],
"resolution": "480p",
"ratio": "9:16",
"duration": 5,
"watermark": false
}
Or through the CLI wrapper:
python3 byteplus_seedance.py \
"@Image1 stands centre frame in a plain studio, turns away from camera, then turns back to face front. Soft even lighting, static camera." \
--model dreamina-seedance-2-0-260128 \
--image "asset://<your-asset-id>" \
--ratio 9:16 --resolution 480p --duration 5 \
--no-watermark --out ./renders
Three request-level rules, each learned from a real 400.
The role is mandatory. Seedance 2.0 requires a role on every image in the content array. Omit it and the API answers role must be specified for image contents. An identity anchor takes "role": "reference_image"; the Seedance 2.0 series tutorial documents the reference-input modes this maps onto.
@Image1 counts by position, never by id. In the prompt field, Image n means the nth asset of that type in the content array, counted from 1 in array order. Referencing by asset id is unsupported. In the CLI, array order is flag order, so the first --image is @Image1.
Leave camera_fixed out. With any reference media present, and an asset reference counts, sending camera_fixed returns HTTP 400 "camera_fixed is not supported for dreamina-seedance-2-0 in r2v, must be empty". Camera discipline is a prompt and post-production concern in this pipeline, never a request flag.
Submit, then poll the retrieve endpoint until status reads succeeded and download content.video_url, exactly as in Chapter 2. Then verify with a contact sheet instead of eyeballing the video:
ffmpeg -i renders/<task-id>.mp4 -vf "fps=2,scale=420:-1,tile=4x3" sheet.png
Ten or so stills on one twelve-cell grid. Compare the face in the first row against the face after the turn. On this pipeline the likeness locked and held through the full turn, and the render cost ~$0.35. That is the entire price of certainty before a production run.
The lock scales past one character. With two registered assets in one request (@Image1 = New Eyes, @Image2 = Ellie) plus a two-performer motion reference, both likenesses held and the roles mapped correctly to the two performers, proven in a 480p test costing ~$0.40. Across a full five-beat film rendered as independent parallel tasks, the assets are what carried both faces shot to shot; identity lives in the request, so every independent render gets the same anchor.
What skips registration: plates, props, and the weapon library
Registration exists because of faces. Everything without a face skips the queue.
Scene plates need no registration. An environment image has no face, so it triggers no moderation and needs no asset id. Pass it straight into the content array as a base64 data URI with "role": "reference_image", and cite it positionally in the prompt: "...in the environment of @Image3." Chapter 6 builds the full scene-plate system on this.
Props and weapons are plain reference images. Same rule: no face, no registration. Keep a weapon library of one clean single-weapon image each, neutral background, side-on. Isolate one weapon per render; a multi-weapon sheet risks the same averaging problem as a 4-view face sheet. Match the prop image's lighting to the scene's.
Combination budget: what one request can carry
Every asset reference spends a slot in a fixed budget. A Seedance 2.0 request carries text plus 0 to 9 images, 0 to 3 videos, and 0 to 3 audios, with no audio-only or text-plus-audio-only combinations, per the Seedance 2.0 series tutorial. A typical production shot in this course spends three image slots (two character assets, one scene plate) and one video slot (the motion reference, Chapter 5), leaving headroom you will rarely use.
The prompt formula that spends those slots well, straight from the model's own guidance: subject plus motion, background plus motion, camera plus motion, most important content first, concrete descriptions over abstract ones.
Cost of carrying an image reference, at the 2.0 rate card (5s clip, image ref present; official pricing tables hold the billing formulas):
| Resolution | 5s clip cost |
|---|---|
| 480p | $0.35 |
| 720p | $0.77 |
| 1080p | $1.73 |
| 4K | $6.65 |
An image reference bills at the higher 7 USD per million tokens; a video input bills at 4.3. The model list confirms dreamina-seedance-2-0-260128 as the id all of these numbers were measured against.
Validate every asset at the 480p rate. Spend the 720p and 1080p money only on shots the edit will keep.
Chapter 5 puts a motion-reference video beside these assets and hits the four 400 errors that guard that combination. The character is registered; next it learns to move.
Register your own character and prove the face-lock
Design one virtual character, produce a single clean front-view crop, register it as a Virtual Portrait Asset under the filename metadata convention, and validate the lock with one cheap 480p render before any production spend.
Expected behaviour
- A console asset entry whose title, group and description were parsed from a filename whose segments contain only letters, digits, underscores and dashes
- Status warehoused on the asset before any render references it
- A 480p validation render whose content array carries the asset:// id with role reference_image and a prompt that turns the character away from camera and back
- A contact sheet extracted from the MP4 showing the same face before and after the turn
- Total validation spend at or under $0.40
PROVE IT Show the console asset entry, the exact request JSON you submitted, and the contact sheet frames demonstrating the same face before and after the turn.
You are registering an AI-generated character so its face holds across renders. Which console path do you take?
What status must a Virtual Portrait Asset reach before you reference it in a render?
The same face PNG is blocked as a raw upload but passes when referenced by its asset:// id. What happened in each case, and what rule do you bank from it?
Show answer
The raw upload hit per-request moderation, which returned HTTP 400 InputImageSensitiveContentDetected.PrivacyInformation because it cannot distinguish an AI face from a real person's. The asset:// reference passed because registration moved the trust decision to a one-time review step, and a warehoused asset is treated as a known quantity. The rule: asset trust and raw-upload trust are separate systems, so any face that recurs across renders gets registered.
↺ re-read: “The drift problem: two ways an unregistered face fails”