Home › Guides › Draft-to-final pipeline

Designing a Seedance draft pipeline: cheap exploration, gated promotion, final render

Updated 2026-10-02

Seedance is sold as several model ids that share one request shape: bytedance/seedance-2.0-mini, bytedance/seedance-2.0-fast, bytedance/seedance-2.0 and bytedance/seedance-2.5. That makes a two-stage pipeline almost free to build: explore on a lighter variant, promote only what you approve, render finals on a heavier one. The hard part is not the API call. It is deciding what counts as "approved", and making sure the pipeline cannot spend more than you intended. This page is the design and the gating code. The variant trade-offs themselves are in the variant comparison; the image-to-video angle is in this guide.

What a draft is for

A draft answers questions that do not depend on final texture: does the composition work, does the motion do what the prompt says, does the subject stay on-model, is the camera move right. It cannot answer questions about fine detail, because a promoted clip is a new generation on a different model and will not be pixel-identical. Keep the two kinds of question separate. If you review drafts for sharpness, you will reject good concepts; if you ship drafts, you will ship the wrong quality.

The stages

  1. Explore. N prompts x short duration x lowest sensible resolution tier on the light variant.
  2. Gate. Automatic checks, then human selection. Only winners continue.
  3. Finalise. Re-submit each winner with the same prompt and inputs on the final model, at the delivery tier and duration.
  4. Verify. Probe the finished file (dimensions and length) and copy it to your storage.

Cost logic, symbolically

Let N be candidate prompts, w the fraction you approve, d and D the draft and final durations, and rd and rf the per-second rates of the draft and final variants. Then:

draft_cost  = N * d * r_d
final_cost  = (N * w) * D * r_f
total       = draft_cost + final_cost
direct_cost = N * D * r_f        # no draft stage

The pipeline wins when total < direct_cost, which rearranges to: d * r_d / (D * r_f) < 1 - w. In words, the draft stage pays off when a draft is cheap relative to a final and you reject a meaningful share of candidates. If you approve nearly everything (w close to 1), drafting is overhead; if you approve one in five, it is a large saving. Read rd and rf from the live table rather than hard-coding them, because host choice moves them.

ModelCheapest hostPriciest hostCheapest isHosts
bytedance/seedance-2.5 (480p)OpenSand
$0.0525 / second
Fal-US
$0.2646 / second
80% lower9
bytedance/seedance-2.0 (2160p)MachGen
$0.59 / second
Fal
$1.5552 / second
62% lower9
bytedance/seedance-2.0-fast (480p)Atlas Cloud
$0.027 / second
Fal
$0.2419 / second
89% lower9
bytedance/seedance-2.0-mini (480p)OpenSand
$0.0104 / second
Fal
$0.0721 / second
86% lower8
seedance-2-mini-unrestricted (480p)OpenSand
$0.0114 / second
SandBase
$0.0721 / second
84% lower3
seedance-2-5-unrestricted (1080p)OpenSand
$0.3482 / second
TOAPIS
$0.5881 / second
41% lower2
seedance-2.0-fast-unrestricted (480p)OpenSand
$0.0344 / second
SandBase
$0.0448 / second
23% lower2
seedance-2-unrestricted (2160p)OpenSand
$0.661 / second
TOAPIS
$0.7966 / second
17% lower2

Per second, before VideoRouter's 2% platform fee. For tiered models each row compares the resolution tier with the widest host-to-host gap. Built 2026-10-02 from the live catalog.

The gating code

The pipeline below separates three concerns: a budget guard that refuses to submit past a limit, an approval callback you supply, and a promotion step that reuses the draft's exact inputs. Rates are passed in, never typed in.

from dataclasses import dataclass, field
import requests

BASE = "https://videorouter.sh/api/v1"
H = {"Authorization": "Bearer llmr_sk_live_..."}
DRAFT_MODEL = "bytedance/seedance-2.0-mini"
FINAL_MODEL = "bytedance/seedance-2.5"

class BudgetExceeded(Exception): pass

@dataclass
class Budget:
    limit: float                       # your own ceiling, in account currency
    rate: dict                         # {"model-id": per-second rate} from the live table
    spent: float = 0.0
    def charge(self, model: str, secs: float) -> None:
        cost = self.rate[model] * secs
        if self.spent + cost > self.limit:
            raise BudgetExceeded(f"{self.spent + cost:.2f} > {self.limit:.2f}")
        self.spent += cost             # billed once, at creation

@dataclass
class Candidate:
    id: str
    prompt: str
    inputs: dict = field(default_factory=dict)   # e.g. {"start_image_url": ...}
    draft_job: str | None = None
    final_job: str | None = None

def submit(model, c: Candidate, secs, budget, **extra):
    budget.charge(model, secs)         # raises before any request is made
    r = requests.post(f"{BASE}/videos", headers=H, timeout=30, json={
        "model": model, "prompt": c.prompt, "duration_secs": secs,
        **c.inputs, **extra})
    r.raise_for_status()
    return r.json()["id"]

def pipeline(cands, approve, budget, d=3, D=8):
    for c in cands:                                    # stage 1: explore
        c.draft_job = submit(DRAFT_MODEL, c, d, budget, resolution="480p")
    # ... poll every draft_job to completion (free), download, auto-check ...
    winners = [c for c in cands if approve(c)]         # stage 2: gate
    for c in winners:                                  # stage 3: finalise
        c.final_job = submit(FINAL_MODEL, c, D, budget, resolution="1080p")
    return winners

Two details are deliberate. The budget check runs before the request, because creation is the billing event; a guard that runs after the fact is only a report. And inputs travels unchanged from draft to final, so the final render sees the same start_image_url and prompt. The resolution strings above are placeholders: the 2.5 model page lists specific tier names, and an unsupported combination is ignored rather than rejected, so copy values from the page for the host you use and verify the output size.

Automatic checks before a human looks

Humans then choose from a contact sheet. Record the decision with the candidate id, because those labels tell you which prompt patterns survive and let you shrink N next round.

Failure handling inside the pipeline

Treat each submission as a ledger entry: write the candidate row before calling the API and the job id after, so a crash cannot lead to a blind re-submit. Never retry a create after a dropped connection; reconcile instead. A promoted job that fails is a fresh decision: failed jobs whose every upstream host failed are not billed, so re-submitting is cheap, but read the error first. The error side is covered in the retry guide.

When not to draft

Start by running ten candidates through the loop with a hard Budget, then measure your real approval rate w and recompute the break-even. Create a key with a monthly cap as a second line of defence behind your own guard.

Frequently asked questions

Is a Seedance draft pixel-identical to its final render?

No. Variants are different models, so a promoted prompt is a new generation. Use drafts to judge composition and motion, and judge detail only on finals.

When does a draft stage save money?

When a draft costs much less than a final and you reject a good share of candidates. If you approve nearly everything, drafting is mostly overhead. Measure your approval rate to decide.

Do the lighter Seedance variants accept the same inputs as 2.5?

Not necessarily. Some variants accept modes that others do not, such as start images or reference arrays, and support differs by host. Confirm on the model pages before relying on a variant as a draft.

Keep reading

Using Seedance is one part of the job.

VideoRouter puts it next to dozens of other video and image models behind one API key, so you can compare providers, prices and fail over automatically. Compare providers on VideoRouter →