Wan 3.0 請求預檢可在排入佇列前捕捉媒體與時長錯誤
A pre-flight check for Wan 3.0 requests: catch media and duration errors before queueing
作者為 Wan 3.0 請求編寫 TypeScript 本地預檢函式,在提交任務前檢查媒體組合、數量、時長及提示詞引用。函式不呼叫網絡,會回傳錯誤和警告;測試涵蓋模式混用、超限及錯誤引用,8 項測試均在 Node.js 20 通過。
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.

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.

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 }
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`);
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`);

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");
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.

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`);
}
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 },
);
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.

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-videoandwan3.0-video-primeside 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.

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");
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.
來源:DEV Community · dev.to