DEV Community

Cover image for A pre-flight check for Wan 3.0 requests: catch media and duration errors before queueing
Kling4 AI
Kling4 AI

Posted on

A pre-flight check for Wan 3.0 requests: catch media and duration errors before queueing

Wan 3.0 is a single model that does a lot of jobs. According to Alibaba Cloud Model Studio's guide, wan3.0-video and wan3.0-video-prime handle text-to-video, first-frame and first-and-last-frame generation, multimodal reference, video editing and video extension. They do it without switching model names. The model works out which job you mean from the type of each item in input.media and from the intent of your prompt.

That flexibility moves a lot of rules into the request itself. Some combinations are invalid. Some are valid but almost certainly not what you meant. A video generation is asynchronous and can take a while, so finding out from a failed task, or from a clip that quietly ignored your edit, is a slow feedback loop.

This post builds a small TypeScript function that checks a request locally before it is sent. It returns a list of errors and warnings, with no network calls. The test suite at the end runs on plain Node.js.

What the API actually constrains

Everything below comes from the "Input media" and "Output video" sections of the Model Studio guide. Each element of input.media has a type and a url.

Screenshot of the Input media table in the Alibaba Cloud Model Studio Wan3.0 guide, listing types and constraints
The input media table from the Model Studio guide. The validator encodes this table.

The rules worth enforcing in code:

Rule Limit
first_frame, last_frame max 1 each
reference_image max 10, each ≤ 20 MB
reference_video max 5, total ≤ 15 s, each ≤ 100 MB
reference_audio max 5, total ≤ 15 s, each ≤ 15 MB
file or link max 1, never both; must be public, no login
Frames vs references first_frame/last_frame cannot be mixed with reference_*, file or link
Materials per request up to 20
duration 2–30, or -1 for smart duration
Video input input + output duration ≤ 30 s

Two more rules are recommendations rather than hard limits. With frame or reference input the guide recommends ratio: "adaptive". The 21:9 ratio is listed only for text-to-video.

Diagram showing two request modes: frame mode with first and last frame, and reference mode with images, videos, audio, file or link; a red barrier between them
Frame inputs and reference inputs live in separate request modes.

The validator

The function takes the prompt, the media array and the parameters. File sizes and clip lengths are optional fields. The validator doesn't download anything, so you supply what you already know from your upload step, for example from fs.stat and ffprobe.

export type MediaType =
  | "first_frame" | "last_frame"
  | "reference_image" | "reference_video" | "reference_audio"
  | "file" | "link";

export interface MediaItem { type: MediaType; url: string; bytes?: number; seconds?: number }
export interface Params {
  resolution?: "480P" | "720P" | "1080P";
  ratio?: "21:9" | "16:9" | "4:3" | "1:1" | "3:4" | "9:16" | "adaptive";
  duration?: number; // 2..30, or -1
}
export interface Issue { level: "error" | "warn"; code: string; msg: string }
Enter fullscreen mode Exit fullscreen mode

The first block enforces mode separation and per-type counts:

const FRAME: MediaType[] = ["first_frame", "last_frame"];
const REFS: MediaType[] = ["reference_image", "reference_video", "reference_audio", "file", "link"];

const hasFrame = media.some(m => FRAME.includes(m.type));
const hasRef = media.some(m => REFS.includes(m.type));
if (hasFrame && hasRef) err("MODE_MIX", "first_frame/last_frame cannot be combined with reference_*, file or link");

const max = { first_frame: 1, last_frame: 1, reference_image: 10,
              reference_video: 5, reference_audio: 5, file: 1, link: 1 };
for (const [t, n] of Object.entries(max))
  if (of(t).length > n) err("COUNT_" + t.toUpperCase(), `${t}: ${of(t).length} given, max ${n}`);
if (of("file").length && of("link").length) err("FILE_AND_LINK", "use a file or a link, not both");
if (media.length > 20) err("TOO_MANY", `${media.length} materials; up to 20 per request`);
Enter fullscreen mode Exit fullscreen mode

Then sizes, total durations and the 30-second budget:

if (sum(of("reference_video")) > 15) err("VID_TOTAL", "reference videos total more than 15 s");
if (sum(of("reference_audio")) > 15) err("AUD_TOTAL", "reference audio totals more than 15 s");

const d = p.duration;
if (d !== undefined && d !== -1 && (d < 2 || d > 30 || !Number.isInteger(d)))
  err("DURATION", `duration ${d}: use an integer 2-30 or -1`);

const vidIn = sum(of("reference_video"));
if (vidIn > 0 && d !== undefined && d !== -1 && vidIn + d > 30)
  err("IN_PLUS_OUT", `input video ${vidIn} s + output ${d} s exceeds 30 s`);
Enter fullscreen mode Exit fullscreen mode

Bar diagram of the 30-second budget: a 12-second source video plus a 20-second output overflows; plus 15 fits
With video input, the source and the output share one 30-second budget.

Checking intent, not just shape

Two mistakes pass every shape check and still waste a job.

A source clip with no intent wording. Editing and extension both send the clip as reference_video. The guide says the prompt has to carry the intent: editing words such as "edit video", "remove", "replace" or "change to", and extension words such as "extend" or "continue". Without them, a reference video is just a reference. The validator can't judge meaning, but it can flag the obvious miss with a warning:

const EDIT_WORDS = /\b(edit|remove|replace|change to|delete|add)\b/i;
const EXTEND_WORDS = /\b(extend|continue)\b/i;
if (of("reference_video").length && !EDIT_WORDS.test(prompt) && !EXTEND_WORDS.test(prompt))
  warn("NO_INTENT", "a reference video without edit/extend wording is read as a reference");
Enter fullscreen mode Exit fullscreen mode

For extensions there is one more arithmetic check. The guide suggests setting duration to -1, or to the source length plus the extension you want. A duration shorter than the source is almost always a mistake, so it gets a warning.

Citations that point at nothing. The Wan 3.0 prompt guide numbers materials by upload order, and counts images, videos and audio separately. The first reference image is "Image 1" and the first reference video is "Video 1", so both can exist at once. With two or more of a kind, the guide says you must use the number. A prompt that mentions "Image 3" when you attached two images will quietly use the wrong material, or none.

Diagram mapping a media array in order to Image 1, Image 2, Video 1 and Audio 1 labels, with separate counters per type
Images, videos and audio each have their own counter, in upload order.

const counts = { image: of("reference_image").length,
                 video: of("reference_video").length,
                 audio: of("reference_audio").length };
for (const m of prompt.matchAll(/\b(image|img|video|audio)\s*(\d+)\b/gi)) {
  const kind = m[1].toLowerCase() === "img" ? "image" : m[1].toLowerCase();
  const n = Number(m[2]);
  if (n < 1 || n > counts[kind])
    err("BAD_CITE", `prompt cites ${m[0]} but only ${counts[kind]} ${kind} reference(s) attached`);
}
Enter fullscreen mode Exit fullscreen mode

Running it

Here is a request with two problems: an extension that overruns the budget, and a citation to a third image that was never attached.

preflight(
  "Extend Video 1 backward: the woman in Image 1 picks up the cup in Image 3.",
  [
    { type: "reference_video", url: "https://cdn.example/take-04.mp4", seconds: 12 },
    { type: "reference_image", url: "https://cdn.example/woman.png" },
    { type: "reference_image", url: "https://cdn.example/cup.png" },
  ],
  { resolution: "720P", ratio: "adaptive", duration: 20 },
);
Enter fullscreen mode Exit fullscreen mode

The test suite uses node:test. It covers a clean multimodal request, mixed modes, the image count limit, reference video totals, extension arithmetic, the missing-intent warning, bad citations, and a combined file-plus-link case. All eight tests pass, compiled with tsc and run on Node.js 20.

Terminal output: eight node:test cases passing, and the demo request reporting IN_PLUS_OUT and BAD_CITE errors
Output of the local test run and the demo request. No API calls were made.

Where this sits in a workflow

In a pipeline, call preflight() right before creating the task. Treat errors as blocking and log warnings next to the job ID. Two more things from the guide are worth adding around it, though they aren't validation:

  • Save outputs quickly. Output video URLs are valid for 24 hours, so download the MP4 as soon as the task succeeds.
  • Pick the model variant on purpose. The guide documents wan3.0-video and wan3.0-video-prime side by side with the same task types. Third-party workspaces present Prime as the speed-first option. The independent wan30.io site, for example, describes its Wan 3.0 Video Prime mode as built for rapid concept testing, with the same 2–30 second range and 480P–1080P outputs.

Screenshot of a third-party Wan 3.0 Video Prime page describing a speed-first multimodal workflow
A third-party workspace page for Video Prime (wan30.io, not an Alibaba product).

Wiring it into the async flow

The Model Studio guide documents a two-step HTTP flow. First you create a task with the X-DashScope-Async: enable header and get back a task ID. Then you poll GET /api/v1/tasks/{task_id} until the task finishes. The pre-flight check belongs before step one, so a rejected request never gets an ID:

const issues = preflight(prompt, media, params);
const errors = issues.filter(i => i.level === "error");
if (errors.length) throw new Error(errors.map(e => `${e.code}: ${e.msg}`).join("\n"));
for (const w of issues.filter(i => i.level === "warn")) log.warn({ code: w.code }, w.msg);

const task = await createTask({ model: "wan3.0-video", input: { prompt, media }, parameters: params });
log.info({ taskId: task.output.task_id, warnings: issues.length }, "queued");
Enter fullscreen mode Exit fullscreen mode

createTask stands in for whatever HTTP client you already use. The point is the order: validate, log the warnings next to the task ID, then queue. When a clip comes back wrong, the warning that predicted it is right there in the log.

One small design choice is worth copying. Return codes, not just messages. Stable codes like MODE_MIX or BAD_CITE let you count which mistakes your team or your upstream prompt builder makes most often, and fix the builder rather than each request.

Limits of a local check

This validator encodes documented limits. It does not know about regional model availability, account quotas or rate limits, and it can't tell whether a public link is actually reachable or still needs a login. The intent check is a keyword heuristic, so a prompt can contain "add" and still not be an edit. Treat its warnings as prompts to look again, not as verdicts.

Even so, most of the failures it catches are cheap to fix and expensive to discover after a generation. That makes a 100-line function a reasonable thing to run on every request.

Top comments (0)