Clone ad — Seedance 2.0
Clone an existing video ad for a different product. You analyze the source frame by frame,
transcribe the dialogue, extract the visual style and beat structure, then generate a new
seedance-2.0 video adapted for the user's product.
How this differs from analyze-video:
- analyze-video → the output is a reusable markdown formula saved to
prompt-library/. - clone-video-ad → the output is a rendered video delivered to the user.
Steps 1 to 3 are the same analysis in both. The only charge in that half is the transcript in step 2 — a fraction of a credit, and free on a re-read of the same source. Everything from step 9 on is the real spend.
If shared/ is missing from disk
references/solo-install.md — what a solo install lacks (craft.md, caption-video, the
b-roll scripts, check-novoads-env.sh, MASTER_CONTEXT.md) and how to fetch each. Skip it
inside the full pack.
Read order
This skill links outward more than any other here, and its two biggest targets fan out again (22 and 10 further links). A file reached through another file gets skimmed, not read, so open these four directly, in order. Everything else is lookup.
- This file — workflow, gates, the constraints that bite.
../novoads-api/prompting/prompt-library/seedance-2.md— fields, grid, prompt craft, what the estimate flags. Before composing a prompt, not when one fails.- One formula matched to the source, not all:
seedance-2-ugc.mdtalking-head,-feature-walkthroughdemo,-premium-reveal/-product-heroproduct-only,-studio-lookbookpolished. Same directory as (2). ../../shared/references/craft.md§ 1 — the transcribe-verify doctrine step 12 rests on.
../novoads-api/SKILL.md is the contract, not a step: open it when a response shape or an
error code needs settling.
What this API changes about cloning
The analysis half is nearly untouched: frames and the beat structure are local work on the
user's file, and the transcript is one cheap API call instead of a local install (step 2).
The generation half has three differences worth knowing before you promise anything. All three were established with free 400 probes that reject
before any charge, and re-verified field-for-field against the deployed spec 2.12.0
(2026-08-06):
| The old shape | Here |
|---|---|
| Chain clip 1 → clip 2 → clip 3 as reference videos, so each clip inherits the last | There is no video-to-video path. referenceVideos is 400 (root): Unrecognized key, and references are images only. What holds a series together is passing the same image assetIds to every clip plus repeating the actor tag verbatim — see step 5 |
audioEnabled: true to switch speech on, false for a silent clone |
audioEnabled exists here now, on the two Seedance variants only (added in spec 2.2.0; 400 Unrecognized key on omni-flash, veo-3.1 and sora-2). It defaults to true, so a clone with dialogue needs nothing. Send false only for a deliberately silent clone — and still write the silence into the prose, because the flag mutes the render while the prose is what stops the model staging a talking shot. It does not change the price, and POST /v1/estimates refuses the field |
Upload the source audio as referenceAudios to clone the voice |
There is no voice cloning on this API (400 Unrecognized key). Describe the voice in the prompt — age, accent, pace, energy — and accept that it is a different person's voice. Do not offer the user a voice match you cannot deliver |
Also gone, in the same probe: endFrame, projectId (this
API has products, not projects), duration (it is durationSeconds) and referenceImages
(it is referenceAssetIds).
resolution was on that list and has come back. It is a real field on seedance-2.0 — 480p, 720p, 1080p, 4k, default 720p (verified live against spec 2.12.0, 2026-08-06). A clone should normally match the source's tier, which for a social ad is 720p; going above it is a spend decision (1080p ≈2.5x the base, 4k ≈5x) that gets priced with POST /v1/estimates and approved like any other. 480p costs ≈half of 720p since the 2026-08-07 family reprice — measured live 2026-08-12, exactly half on both seedance-2.0 and seedance-2.5 — so it is a real draft tier, worth offering when a clone is a rehearsal rather than the deliverable. (The older line here, that it cost the same and bought nothing, described the pre-reprice deployment.) A clone rendered as a series pays the multiplier on every clip — check the tier before you fan out. Never send the key on seedance-2.0-mini, which renders 720p only.
And one the old shape got wrong in the other direction: aspect ratio is not
9:16-or-16:9. Seedance takes 16:9 9:16 1:1 4:3 3:4 21:9 — probed live, 1:1
and 4:3 both pass validation — so a square or landscape source clones at its own ratio
instead of being letterboxed into a vertical frame.
Prerequisites
which ffmpeg || echo "MISSING — run: brew install ffmpeg"
./scripts/check-novoads-env.sh
ffmpeg and a working key are the whole list. extract-frames.sh needs ffmpeg; the
transcript comes from POST /v1/transcripts (step 2) and needs nothing installed. The env
check has to return 200 before step 9, and it is worth running first: discovering a bad key
after the user has approved a dialogue script is a bad look.
whisper is optional, and only for working offline — see the fallback in step 2. Do not ask the user to install it before starting. A clean machine can clone an ad.
REST key required. A Novoads MCP connector is not a substitute. If
NOVOADS_API_KEYis missing or still the placeholder, stop before any generation work and tell the user: "Before continuing, create an API key at https://novoads.ai/dashboard/settings?tab=api and paste it into.env." That holds even whenmcp__novoads__*tools are connected and authenticated in the session. Never callmcp__novoads__*tools from this repo's workflows: they are a different surface with different behavior, including the units they quote costs in. Repo installs verify with./scripts/check-novoads-env.sh; a solo install checksNOVOADS_API_KEYin the environment.
Pack version. Every /v1 response carries X-Novoads-Pack-Version; mention a newer pack at https://github.com/novoads/agent-skills only when that header names a version NEWER than this file's metadata.packVersion — equal or older is nothing to say, and it is never a reason to stop.
Workflow
Step 0: Gather inputs
If no source video was handed over, do not ask "which ad?". That puts the work back on
the user at the moment they asked us to do it. Run spy-competitor-ads and say what you are
about to do in one line, with a price and a way out:
No clip attached. I'll sweep Arcads, Creatify and Icon for their video ads and clone the strongest. N credits for the sweeps; I'll price the render before it runs. Say "stop", or drop your own clip instead.
Four things that line carries, and nothing else:
mediaType: "video", and it is not the user's choice. A static sweep returns creatives this skill cannot open, so the mode is fixed here and stated rather than asked. Asking is a question with one correct answer.- The competitors by name, so a wrong guess is corrected before it is paid for. Default to three when you picked them yourself; honor any list the user names.
- The sweep total from a live
POST /v1/estimatesin this session, never a number from memory or from this file — and quoted as sweeps, not as the run. A video render costs multiples of a sweep, and step 9 is where that number gets its own gate. A line that implies the sweep total is the whole cost is wrong even when every figure in it is real. - One word that stops it, and the escape: they can hand over their own clip instead.
Then act. It is a proposal with a veto, not a question and not a menu. spy-competitor-ads
returns file paths and a ranked top three; take the top one unless the user picked otherwise,
and carry on into step 1 with that file.
If the sweep comes back empty, say so in one line and stop. It is a charged, correct result — that brand has no fetchable video ads running — not a reason to re-sweep or to widen the mode.
| Input | Required | Notes |
|---|---|---|
| Source video | yes | The ad to clone. .mp4, .mov, .webm. Handed over, or swept for above |
| Product photo | strongly recommended | Becomes startImageAssetId or @Image1 in referenceAssetIds. Without one, see "no photo" below — the answer is not "let Seedance invent it" |
| Product / offer description | if no photo | Features, audience, selling points. Used to rewrite the dialogue and the product references, and to generate a still if they want one |
| A photo of the person | optional | Only if the clone is a series — it is what holds one face across clips |
| Brand voice | optional | Check MASTER_CONTEXT.md first. If its brand blocks are empty, ask for tone and audience |
Check references/ at the repo root before asking. references/products/ for product
shots, references/influencers/ for people, references/aesthetics/ for style boards. If
the photo is already there, offer to use it. These folders ship empty — a .gitkeep is not
a product shot, so an empty listing means "ask", not "there is nothing to use".
No photo? Three routes, in this order — and the last one is the expensive one.
- A real photo of the real product. Always the best clone. Ask for it first.
- Generate a still with
POST /v1/images— one synchronous call, priced throughPOST /v1/estimatesand consented to like any other spend. Its response carries anassetIdthat goes straight intoreferenceAssetIds(step 10). This is the right answer for a concept product, a product that does not exist yet, or a user who simply has no usable shot. It does not eat a render slot: images have their own concurrency ceiling, counted apart fromPOST /v1/videosin both directions, so sourcing a still never delays a clip and clips in flight never delay the still. - Describe it in the prompt text only. Say plainly what this means: Seedance invents a design, renders it, and charges for it, and the same product will look different in every clip of a series. Reach for this when the product is generic enough not to matter.
If they hand over only a video and say "clone this for my product", ask for a photo or a description before going any further. Cloning a style onto a product you cannot see is guesswork the user pays for.
Step 1: Extract frames and audio
Reuse the analyze-video script. Do not duplicate it.
bash "skills/analyze-video/scripts/extract-frames.sh" \
"<source_video_path>" "/tmp/clone-video-ad-analysis" <num_frames>
| Source duration | Frames |
|---|---|
| Under 10s | 8 |
| 10–20s | 12 |
| 20–30s | 16 |
| Over 30s | 20 |
| The user asks for extreme precision, at any duration | 40–100 |
The script takes any count as its third argument — evenly spaced, no cap — so the top row costs context, not credits. Use it when the ask is explicitly for a frame-by-frame read.
Outputs: frame_001.jpg … frame_NNN.jpg, audio.wav (16 kHz mono), and metadata.txt
with duration, resolution and fps. Read the duration — step 5 branches on it.
Step 2: Transcribe the source
The words come from the API. Upload the source and transcribe it — two calls, nothing installed:
# 1. mint an upload slot for the SOURCE video (measure the bytes, never estimate them)
curl -sS -X POST https://api.novoads.ai/v1/uploads \
-H "Authorization: Bearer $NOVOADS_API_KEY" -H "Content-Type: application/json" \
-d "{\"contentType\":\"video/mp4\",\"sizeBytes\":$(stat -f%z source.mp4)}"
# 2. PUT the bytes with EXACTLY the returned headers, then:
curl -sS -X POST https://api.novoads.ai/v1/transcripts \
-H "Authorization: Bearer $NOVOADS_API_KEY" -H "Content-Type: application/json" \
-d '{"assetId":"<the source assetId>"}'
POST /v1/uploads takes video/mp4, video/quicktime and video/webm — the three
formats step 0 accepts. The transcript comes back in the same response, not polled for:
text, words[] with start/end, segments[], an srt, and the detected language.
Uploading the source is for READING it, never for rendering from it. See the boundary
in step 10 — the source's assetId must never reach referenceAssetIds,
startImageAssetId, or any other generation input.
Record the full transcript, the per-segment timestamps and text, the total word count, and
the language. The language matters twice: it is what you send as language, and it is what
you write the adapted prompt in. The segments[] are beat candidates — they are cut at
the source's own pauses, which is where step 3's beat map usually wants its boundaries.
Two things worth knowing:
- Timings are in seconds, matching
metadata.txt. No conversion. - Re-transcribing the same source is free —
creditsCharged: 0, served from storage. Do not cache it by hand to avoid a charge that will not happen.
Offline fallback — whisper. Only when there is no key or no network:
import whisper
result = whisper.load_model("base").transcribe("/tmp/clone-video-ad-analysis/audio.wav")
Two traps this pack has already paid for, the same ones broll-overlay documents:
- whisper reports milliseconds where this API reports seconds. Convert, or every beat boundary lands 1000× off.
whisper-cliwith no model downloaded returns an EMPTY transcript rather than an error. Homebrew installs the binary with a test stub, so the failure is silent and reads exactly like an ad whose every line went missing.pip3 install openai-whisperplus a model download — the binary alone is not enough.
A silent source. audio.wav is left unwritten when there is no audio stream:
extract-frames.sh catches ffmpeg's failure, prints No audio stream found (silent video)
and exits 0, so the missing file is the only signal you get. On the API path the same
thing surfaces as a transcript with no words.
test -f /tmp/clone-video-ad-analysis/audio.wav || echo "silent source — skip to step 3"
If no speech is detected — or there was no audio stream to begin with — note it and skip
the dialogue work in step 7. The clone is a visual-style clone, and it has to declare its
own silence twice: audioEnabled: false on the call and the silence written into the
prompt text (steps 6 and 8).
Step 3: Compressed analysis
There is a hosted alternative, and it is not the default. POST /v1/analyses returns the
same class of read (hook boundary and the observable signal behind it, beat timeline,
on-screen text, casting, layer zones labelled by where their pixels come from) in one
synchronous call, flat 1 credit, priced through POST /v1/estimates with
{"kind":"analysis"} first like every other spend. The pass below costs nothing, covers the
whole runtime, and you already have the frames on disk from step 1, so it stays the front
door. Reach for /analyses when ffmpeg is not installed, when the clone needs zone labels
rather than prose (sourceType tells you which zones are generated footage and which are
overlay, which is the one thing a frame read guesses at), or when this pass has already
failed. Two things to say out loud if you offer it: it is a charge, and it defaults to the
first 20 seconds, so a 40-second source needs maxSeconds raised or it reads the hook only.
Read all the extracted frames. For each, note:
Structure and pacing — how many beats, what the arc is (hook → demo → verdict? reveal → detail → CTA?), how long each beat lasts against the segment timestamps.
Camera and framing — POV (selfie, handheld, tripod, propped phone, over-the-shoulder), framing per beat (wide, medium, close-up, macro), movement (static, pan, dolly, handheld shake), and any signature move: "leans into camera", "tilts the product toward the lens".
Edit style — transition type, visual rhythm, recurring motifs ("every other beat is an extreme close-up").
Dialogue and script structure — hook format, speech pattern (casual or formal, filler words, trailing thoughts, mid-sentence cuts), how many spoken lines, how many silent beats, CTA style.
Tone and energy — emotion words, the energy arc, the speaker's relationship to the viewer: friend, expert, skeptic, fan.
Lighting and technical quality — light source and direction, camera class, deliberate flaws, audio character.
Product references — how the product is physically shown, which claims are called out, what labels and text are visible on screen.
What makes this ad distinctive — the 2–3 traits that make it recognisable. These are the ones that MUST transfer; everything else is negotiable.
This analysis stays in the conversation. It is not saved as a template file — that is analyze-video's job.
Step 4: Present the analysis, and make it a contract
Show the breakdown before doing anything else, and derive the payload from it out loud, so the user can correct a wrong reading before it costs anything:
📋 Source video analysis
Duration: 13.6s | Beats: 4 | Dialogue: 31 words (en) | Style: skeptic-converted UGC
Beat map:
[00:00–00:03] HOOK — close-up, deadpan, "opening line"
[00:03–00:07] SHOW — tilts product to camera, "feature call-out"
[00:07–00:10] DEMO — (silent) applies the product, close-up on texture
[00:10–00:13] VERDICT — back to medium, "closing line + CTA"
Defining traits (must transfer):
1. [trait] — because [what it does for the ad]
2. [trait] — because […]
3. [trait] — because […]
What transfers to your product:
✅ Beat structure, pacing, camera angles, edit style, tone, energy
✅ Dialogue pattern, adapted to your product
✅ Lighting and technical-quality cues
What gets swapped:
🔄 [source product] → your product
🔄 [source claim] → your claim
🔄 [source brand mention] → your brand
What that means for the call:
• 13.6s ≤ 15s → single clip, durationSeconds: 14
• vertical source → aspectRatio: "9:16"
• speech detected (en) → language: "en", dialogue gate applies
• product photo given → referenceAssetIds: ["@Image1 = your product"]
Proceed with the adaptation? (yes / adjust)
Wait for the answer. This is a reading of their video, not an approval to spend.
Step 5: Decide the generation mode
┌─ Source ≤ 15 seconds?
│ YES → one clip. durationSeconds = the source duration, rounded to an integer in 4–15.
│ NO → two routes, and the USER picks between them.
│ See "Over 15 seconds is a choice" below. Do not default to either.
│
├─ Do you have a product image — theirs, or one you generated?
│ YES → upload it (or reuse the assetId POST /v1/images already returned), then pick
│ one mode:
│ startImageAssetId — the ad opens ON the product, held up or on a surface.
│ It animates that photo as the literal first frame.
│ referenceAssetIds — the ad builds a scene the product was never photographed
│ in. It composites the references instead, and the prompt
│ addresses them as @Image1, @Image2 … by array position.
│ Never both: a body carrying both is a 400 that says they are separate modes.
│ NO → offer POST /v1/images before falling back to prose (step 0). Prose-only means
│ Seedance invents a design, renders it, charges for it, and re-invents it on
│ every clip of a series.
│
├─ Is it a series (the >15s route the user picked, not every >15s source)?
│ YES → referenceAssetIds, and pass the SAME ids to every clip:
│ @Image1 = the product, @Image2 = the person if one is on screen.
│ Repeat the actor tag verbatim in every clip. Never "the same woman".
│ NO → either mode.
│
└─ Does the source speak?
YES → the line is rendered and lip-synced in this same call. Gate 1 (step 7) applies.
NO → BOTH halves: send audioEnabled: false, AND declare the silence in the prompt
prose ("silent b-roll, no spoken dialogue"). The flag mutes the render; the
prose is what stops the model staging a talking shot. See step 8.
Over 15 seconds is a choice, not a default. A source longer than one clip's ceiling has two routes, and which one is right depends on what the user is buying. Present both with the tradeoff and let them pick — do not choose one and mention the other in passing.
| Route | What survives | What it costs |
|---|---|---|
| One-shot compression — the beats become jump cuts inside a single ≤15s render | Continuous voice, no stitching, one charge | The clone no longer matches the source's runtime or pacing, and those are transferable traits too |
| Multi-clip series — split at beat boundaries taken from the beat map, never from arithmetic | The source's runtime and pacing | Roughly twice the spend: a charge per clip, and any resolution tier paid per clip |
The evidence that one-shot is viable is #13's measured A/B — one render carrying its beats
internally came back at roughly half the spend of a stitched arm whose voice was absent for
half its runtime. Cite that as viability, not superiority. The stitched arm anchored
each clip with startImageAssetId, a different mechanism from the shared-referenceAssetIds
series described below, so it does not measure this route at all.
A series is held by references, not by chaining. The old pattern — render clip 1, feed
its output in as clip 2's reference video — has no path here: references are images only.
That is not a downgrade. This repo's own animation rule says the same thing on its own
merits: "Don't chain by using an animated end-frame as the next beat's anchor — drift
compounds" (skills/claymation-ad/references/formulas.md). Every clip
anchoring to the same approved stills is the more stable pattern, and because the assetId
is durable across calls, models and sessions, it costs one upload for the whole series.
Re-uploading the same bytes mints a second asset and throws away the anchor.
Fire the clips of a series concurrently, not sequentially — nothing downstream depends
on an earlier clip's output any more. Five generations per organization may be in flight at
once; a sixth comes back 429 with details.reason of concurrency_limit, which is
solved by waiting for a slot and not by backing off harder.
Offer the mini draft. seedance-2.0-mini is the same grid and the same prompt at half the
price, back in 2–3 minutes instead of 3–8. Its fields are the same except resolution,
which it does not take at all — it renders 720p, and sending the key is a 400. A clone is
exactly the case
for it: the first render is where you find out whether your reading of the source survived
into the prompt. Draft on mini, re-price with model set to the final tier, and show both
numbers side by side. SKILL.md makes the tier an explicit question, asked once per
workflow.
Ask how many variations, and which kind — here, not at generation time. Default 1. Two different things share the word: the same script rendered N times (identical payload, seed-level variety only) or N script variants (distinct dialogue adaptations on one beat structure). A clone usually wants the second. Ask it now, because everything downstream needs the answer: step 6 writes N scripts, step 7 gates them together, and step 9 prices each one before the single yes. Asking at step 11 means the user approved a spend for one prompt and is then handed three.
Tell the user which mode you picked and why.
Step 6: Adapt for the user's product
The creative core. Working from step 3:
Dialogue adaptation (if the source speaks):
- Keep the same conversational pattern. Question hook in, question hook out. If the source runs on filler — "like", "okay so" — keep the filler; it is the style.
- Keep the same number of spoken lines and the same silent-beat placement.
- Keep the same energy arc: excited → calm, flat, or building.
- Replace product-specific references with the user's product name, features and claims.
- Match each line's word count within about ±3 words so the pacing survives.
- Read it back at a natural pace against the target duration: 2.0 words per second measured
(~13 chars/sec with spaces), so a
D-second clip holds about2.0 × (D − 0.5)words. - Script variants. If they asked for N script variants rather than N renders of one script (step 5), write N distinct adaptations that share the beat structure, the silent-beat placement and the per-line word counts, and differ in the hook angle, the claim emphasized, or the CTA. They are alternative readings of the same source, not escalating rewrites — do not let variant 3 drift into a different ad.
Visual adaptation:
- Keep the camera work, the framing per beat, and the edit style you analysed.
- Replace the product description: physical appearance, colours, materials, label details.
- Keep the setting, the lighting and the atmosphere.
- Keep the person description, unless the user wants a different persona.
- Keep the technical-flaw cues — phone quality, mic character, imperfect light. They are what makes it read as real.
Prompt composition. Read seedance-2.md before
composing, and the closest formula for structure —
seedance-2-ugc.md for a talking-head source,
seedance-2-feature-walkthrough.md for
a fast demo, seedance-2-premium-reveal.md
or seedance-2-product-hero.md for a
product-only source, seedance-2-studio-lookbook.md
for a polished voiceover source. Cloning onto a different model reads that model's own grid first, never a Seedance formula carried across: sora-2.md when the source's hook speaks from frame one (it measured no leading silence, where Seedance front-loads 3–5s of it), veo-3-1.md when the user names Veo — neither takes referenceAssetIds, so a clone carrying both the actor and the product stays on Seedance. ugc-selfie-style.md is the cross-model UGC guide: its Core principles transfer, its per-model formulas do not.
If the mode is a one-shot compression, read seedance-2-ugc-v2.md as well — for structure and mode only. Take the beats-inside-one-render mechanics from v2 and the prompt craft from v1, which is the scope that file's own contract sets — and say which came from which when you present the prompt, so a wrong borrow is visible before it renders. Your source beat map wins over its beat doctrine. v2 defaults to one-shot and tells you to keep silent beats out of the base; a clone is not writing a base video, it is reproducing one. If the source has a silent beat, the clone has a silent beat, and v2 does not get a vote on that.
Then:
- Order: Subject + Action + Camera + Style + Constraints, written as flowing prose.
A bulleted prompt or a run of
Label: valuepairs comes back rendered as literal text on screen. - Keep prompts between 100 and 260 words. Shorter prompts produce vague results; longer ones overwhelm the model and cause it to lose focus on key details.
- Timestamps —
[00:00],[00:05]— for multi-beat pacing, one main action per block. @Image1wherever the prompt points at a reference, matching the array order you will send. A token past the end of the array is refused before the charge. If the mode isstartImageAssetId, use no tokens.- Consistency anchors: the product from
@Image1remains visually unchanged in every shot, keep the outfit unchanged across all cuts. - The label hold, whenever a label, package, bottle, box or screen is visible: the product label remains perfectly sharp and identical to the reference image with its text unchanged and fully legible.
- One primary action per shot, two or three comma-joined cues on it.
then/and then/followed byrenders as a smear — split it into two shots. - No forbidden words:
cinematic,professional,stunning,8k,studio,perfect. Nothing on the API rejects or reports them — this is craft advice, and the reason to drop them is the render. Describe the real thing instead: the light source, the surface, the flaw. - Write it in the source's language if that is what the user wants rendered. Nothing on the API pushes back on a Spanish or Portuguese prompt.
Duration: source ≤ 15s → match it, rounded to an integer in 4–15. Source > 15s → whichever
route the user picked in step 5: one ≤15s render for a compression, or one duration per clip
for a series. Never re-decide it here. Validate against the spoken word count; SKILL.md
carries the table.
Step 7: Dialogue confirmation gate
Mandatory for any clone that speaks. The line inside the double quotes is what the actor says out loud in the finished file, and it cannot be changed afterwards without paying for the render again. Use the format from SKILL.md:
📝 Dialogue script (please confirm before I generate)
1. [HOOK] "adapted line matching the source's hook pattern"
2. [SHOW] "adapted feature call-out for the user's product"
3. [DEMO] (silent beat — physical demonstration, no dialogue)
4. [VERDICT] "adapted closing line / CTA"
Total spoken words: ~N | Target duration: Xs | language: en | Fits at natural pace: ✅
Approve this dialogue? (yes / edit / rewrite)
Rules:
- This gate is separate from the cost gate, and neither implies the other. Approving the analysis is not approving the sentences, and approving the sentences is not approving the spend.
- Never infer approval from an earlier yes about tone, beat map or mode.
- If they say edit, revise and re-present until they approve.
- Skip it only when the source is silent, say why you are skipping it, and check that both
halves of the silence are in place:
audioEnabled: falseon the call and the silence written into the prompt prose. Neither one covers for the other. - On a series, the gate covers every clip, presented together.
- On script variants, the gate covers every variant, also presented together — one block, one approval, however many scripts are in it. Never one gate per variant: the point of the block is that they are compared against each other before any is priced.
Step 8: Language and audio
Three decisions:
language— declared, not controlling. The prompt is what decides the spoken language: the render says whatever the quoted line says, and nothing rejects a body whoselanguagedisagrees with it. Default the field from thelanguagethe transcript returned (step 2), state it in the gate above, and then actually write the dialogue in that language — the field records the ad for later reporting, it does not translate anything.- Whether the clone speaks at all. A silent source clones silent, and that takes
both halves:
audioEnabled: falsein thePOST /v1/videosbody, and the silence written into the prompt prose (silent b-roll, no spoken dialogue). The flag mutes the render; the prose is what stops the model staging a talking shot. Prose alone pays for a generated voice track you then discard. Quoting on-screen text in the prompt is not enough either — the model reads a quoted string as a line to speak, which is the opposite of what you asked for. The flag is Seedance-only, andPOST /v1/estimatesrefuses it: muting is not a discount. - The voice. It will not be the source's voice — there is no voice cloning here. If voice matters, describe it in the prompt (age, accent, pace, energy) and tell the user plainly that it is a soundalike, not a match.
Step 9: Price it — the cost gate
Never state a credit cost from memory, and never generate before showing a number that
came from a live call in this session. There are no rate tables in this repo, in
MASTER_CONTEXT.md, or in the logs.
curl -sS -X POST https://api.novoads.ai/v1/estimates \
-H "Authorization: Bearer $NOVOADS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind":"video","model":"seedance-2.0","durationSeconds":14,"language":"en","prompt":"<the adapted prompt>"}'
Add "resolution":"1080p" to that body when the clone is going above the model's default —
see the bullet below.
Returns credits, balance, sufficient and a warnings array, plus shortBy and
topUpUrl when it is short. The body is strict and takes exactly six fields: kind,
prompt, model, durationSeconds, language, resolution — checked field-for-field
against CreateEstimateRequestVideo in spec 2.12.0 (2026-08-06). aspectRatio,
audioEnabled, the asset fields and productId are a 400 here.
- Pass
modelexplicitly. It defaults toseedance-2.0, and mini is half the price — pricing the wrong tier is a quote that disagrees with the invoice. - Pass
resolutionwhenever the tier is above the default. It is the second price axis: the high tiers are their own credit schedules, not a surcharge on the low one. Leave it out and you quote720pand invoice whatever you actually render. Never send it onseedance-2.0-mini, which renders 720p only. languageis recorded, not priced. It does not change what the model is sent and it does not move the number — the spoken language comes from the quoted line in your prompt. Send it anyway: it is what makes "how do our Spanish ads perform?" answerable later.- The
warningsare advice, never a verdict. Nothing here can refuse or reprice a call, so a weak clone prices, charges and renders exactly like a good one. They are substring matches and they do false-positive — read each against the prompt, and say so when you override one. Step 6's checklist is what actually stands between the two. - Price every clip and every variant, each with its own call. A 3-clip series at 2 script variants is 6 estimates and 6 charges. The estimates are free and fire concurrently, and they are also the per-model length check and the free lint — a prompt that skipped one is a prompt nobody checked, and there is no second chance at submit time. Show the per-call number, the count and the total.
- Warn when the total exceeds
balance, and quoteshortByandtopUpUrlwhen they come back.sufficientis a snapshot, not a reservation.
Show the number, the count, the total and the balance. Get one yes covering the whole set. Then generate.
Step 10: Resolve the product and upload the references
GET /v1/products→ read.items[](not.products— verified live 2026-08-05) →productId. Default to the product named inMASTER_CONTEXT.mdunder "My workspace"; with exactly one product, save it there; with none, omit the field — it is optional. There is no folder or project ritual to run: folders are read-only on this API and there are no projects at all.Upload each reference once:
curl -sS -X POST https://api.novoads.ai/v1/uploads \ -H "Authorization: Bearer $NOVOADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contentType":"image/jpeg","sizeBytes":248193}'Returns
assetId,uploadUrl,methodandheaders. PUT the raw bytes touploadUrlwith exactly those headers:curl -sS -X PUT "$UPLOAD_URL" \ -H "Content-Type: image/jpeg" \ -H "Content-Length: 248193" \ --data-binary @product.jpgBoth are signed into the URL:
image/jpeg; charset=utf-8is a403, and so is aContent-Lengththat does not match the bytes. MeasuresizeBytes, do not estimate it.A still you generated is already an asset — do not upload it again.
POST /v1/imagesreturnsassetIdbesideurlon every image (deployed spec2.11.0). Pass that id straight intoreferenceAssetIdsorstartImageAssetId. Downloading the bytes and pushing them back throughPOST /v1/uploadsmints a second asset and throws away the anchor the rest of the workflow is pinned to. Theurlexpires in 3600 seconds; theassetIddoes not — chain from the id, never from the URL.Keep the
assetId. It is durable across calls, models and sessions — the whole series, the mini draft and the final render all reuse it. The presigned upload URL expires in 900 seconds; the id does not.If a reference's longest side is under 1024 px, upscale with Lanczos to 1080 px on the long side and re-encode as RGB JPEG at quality 90–95. (No minimum is documented for this API; the practice carries over from a sibling API and is unverified here.)
The source ad: upload it to READ it, never to RENDER from it.
POST /v1/uploads takes video, and step 2 uses that on purpose — the source is uploaded so
POST /v1/transcripts can read its words. That is the whole extent of it. The source's
assetId must never appear in referenceAssetIds, in startImageAssetId, or in any other
generation input. It is input to your eyes and to the transcriber; it is never input to
the model that renders.
This is not a convention you have to remember — the API enforces it, for free, before any
charge. A source video's id in referenceAssetIds comes back:
400 invalid_input — referenceAssetIds accepts image uploads only; <id> is not one.
Video and audio references are not available on this endpoint.
Generation references are images only: image/jpeg, image/png, image/webp. There is
no video-to-video path on this API, so a clone is never built by feeding the original in.
What carries a source's style into a clone is your reading of it, written into the prompt.
Step 11: Generate
curl -sS -X POST https://api.novoads.ai/v1/videos \
-H "Authorization: Bearer $NOVOADS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2.0",
"prompt": "<the adapted prompt>",
"durationSeconds": 14,
"aspectRatio": "9:16",
"language": "en",
"referenceAssetIds": ["<product assetId>"],
"productId": "<uuid>"
}'
startImageAssetId instead of referenceAssetIds when that is the mode. Never both.
- Returns
202withjobId,status,creditsChargedandmodel. Nowarningshere — but the estimate does return them (verified live 2026-08-05). Collect the craft advice at gate 2; there is no second chance at submit time. - Set
aspectRatioanddurationSecondsexplicitly. Seedance defaults to16:9and to 5 seconds, and neither is what a cloned ad wants. - Carry
resolutioninto this body whenever the estimate carried it. The estimate priced the tier; this call is what renders it. Quote1080pat gate 2 and then omit the key here and the user approved one video and rece
…(truncated)