DEV Community

fei gao
fei gao

Posted on

Building Wan 3.0: The Hard Parts of an AI Video Workspace

A prompt box makes an AI product look simple. The user writes a sentence,
clicks Generate, and waits for a video.

The real system is less tidy. A request can outlive the browser tab, a provider
can accept a job and fail later, and a retry can accidentally create a second
billable task. Add multiple models, several input modes, and usage-based
pricing, and the prompt box becomes the smallest part of the product.

These are some of the engineering decisions behind
Wan 3.0, the AI video workspace we have been building for
text, image, frame, and reference-based generation. This is not a launch post
disguised as a tutorial. It is a practical look at the parts that took more
thought than the interface suggests.

One form is not one generation workflow

Text-to-video and image-to-video may end with the same file type, but they do
not begin with the same contract.

A text request needs a prompt, aspect ratio, resolution, and duration. An
image-to-video request also needs an uploaded asset. A frame transition needs
two ordered images, while reference-based generation may accept a clip or a
set of visual references. Model support differs as well.

We represent those paths as explicit scenes rather than stretching one loose
payload across every model:

type VideoScene =
  | 'text-to-video'
  | 'image-to-video'
  | 'frames-to-video'
  | 'reference-to-video'
  | 'video-edit'
  | 'video-extend'
  | 'video-upscale';
Enter fullscreen mode Exit fullscreen mode

Each model declares the scenes and fields it supports. The UI can then adapt
to the chosen workflow, and the server can reject combinations that do not
make sense before contacting a provider.

That early validation matters. An upstream API error is slower, harder to
explain, and sometimes more expensive than a local validation error.

The AI video workspace needs a stable provider boundary

Provider APIs disagree about nearly everything: parameter names, callback
formats, status values, result shapes, and whether polling or webhooks are the
preferred completion path.

Letting those differences leak into the product would couple every form and
task screen to a specific vendor. Instead, Wan 3.0 puts a small adapter around
each provider:

interface AIProvider {
  readonly name: string;
  readonly supportsWebhook: boolean;

  generate(params: AIGenerateParams): Promise<AIProviderResult>;
  query?(providerTaskId: string): Promise<AIProviderResult>;
  verifyWebhook?(request: Request): Promise<AIProviderResult>;
  cancel?(providerTaskId: string): Promise<void>;
}
Enter fullscreen mode Exit fullscreen mode

The rest of the application works with one internal result shape and a small
set of task states. Provider-specific code stays at the edge.

This does not make providers interchangeable. Models still have different
inputs and capabilities. It does, however, give the application one place to
translate those differences instead of scattering conditional logic across
the codebase.

An AI request is a durable task, not a long HTTP call

Video generation is asynchronous by nature. Treating it like a normal request
and keeping the browser waiting creates fragile behavior for both the user and
the server.

Our task lifecycle uses five states:

pending -> processing -> succeeded
                      -> failed
                      -> canceled
Enter fullscreen mode Exit fullscreen mode

Providers can complete synchronously, through polling, or by webhook. Those
transport details are normalized into the same task record and result format.
The browser can leave, return later, and read the current state from history.

The simplified flow looks like this:

scene + model + inputs
        |
        v
validation and credit estimate
        |
        v
task creation + credit reservation
        |
        v
provider adapter
        |
        v
webhook or polling
        |
        v
result history or automatic refund
Enter fullscreen mode Exit fullscreen mode

Persisting the task also gives us a useful audit trail: selected model,
provider, normalized input, pricing snapshot, cost, timestamps, and terminal
result all belong to the same operation.

Idempotency protects users from double generation

Retries are normal. A user can double-click, the network can time out after
the server accepts a request, or a client can retry because it never received
the first response.

For a paid generation, “probably only once” is not good enough.

Every create request carries an idempotency key. We also calculate a
fingerprint from the model, scene, and validated parameters. If the same key
returns with the same fingerprint, the existing task is reused. If that key
appears with different input, the request is rejected.

The fingerprint check closes an easy-to-miss gap: an idempotency key should
identify one operation, not become a container for whichever payload arrives
last.

Pricing must be part of task creation

Usage-based products should show the cost before the expensive operation
starts. The harder requirement is making sure the displayed estimate and the
recorded charge use the same calculation.

Wan 3.0 calculates credits from the selected model and validated settings.
Depending on the model, duration, resolution, and other options can change the
result. The task stores both the calculated cost and a pricing snapshot, so a
later configuration change does not rewrite the meaning of an older task.

Task insertion and credit reservation happen in one database transaction. If
either step fails, neither should survive on its own. That keeps us away from
two awkward states:

  • a provider job exists, but no usage was recorded;
  • credits were deducted, but no task exists for the user to inspect.

The UI benefit is straightforward: the number shown before submission is tied
to the task the user sees afterward.

Failed jobs need an exact refund path

External generation can fail after a provider has accepted the request. It can
also time out or send the same callback more than once. A refund handler must
therefore be safe to repeat.

Our refund path locks the task record, checks whether the task is already in a
terminal or refunded state, writes a refund transaction, restores the balance,
and marks the task as refunded. All of that happens inside a transaction.

The goal is not “try to refund.” It is a narrower invariant:

A failed generation that reserved credits can restore them once, even if the
failure signal is processed more than once.

This is one of those backend details that becomes a product feature. The user
does not need to know about row locks or duplicate webhooks. They only need to
see that a failed render did not consume their balance.

What we would design first next time

If we started another asynchronous AI product tomorrow, we would define four
things before polishing the generation form:

  1. The task state machine. Decide which states are terminal and which transitions are allowed.
  2. The provider contract. Normalize behavior and results, while letting model capabilities remain explicit.
  3. The billing invariant. Tie task creation, cost snapshots, reservations, and refunds together.
  4. The retry contract. Add idempotency before the first duplicate request reaches production.

None of these decisions produces a dramatic screenshot. Together, they make
the simple screenshot honest.

FAQ

What is Wan 3.0?

Wan 3.0 is a browser-based workspace for generating AI videos and images. Its
video workflows include text, image, frame-pair, and reference inputs where
the selected model supports them.

Does every model support every input mode?

No. Models declare their supported scenes and parameters. The interface and
server validation use that configuration to prevent unsupported combinations.

What happens when a generation fails?

If a task reserved credits and later fails, the refund flow restores those
credits once and records the refund against the task.

Why use both an idempotency key and a request fingerprint?

The key identifies the operation. The fingerprint confirms that repeated uses
of the key contain the same model, scene, and input. Together, they prevent a
retry from silently becoming a different generation.

Conclusion

Building an AI video workspace is mostly an exercise in managing uncertainty:
slow jobs, changing providers, variable costs, duplicate requests, and partial
failures. A clean prompt box is valuable, but it only stays clean when the
task, provider, and credit systems underneath it have clear contracts.

You can try Wan 3.0 at wan3.io. If you are building an
asynchronous AI product, I would be interested to hear how you handle provider
drift, retries, and usage reconciliation.

Top comments (0)