DEV Community

TheophilusHawkins9265
TheophilusHawkins9265

Posted on

Video Generation UI: Capability-Gated Forms and Asynchronous Status That Survive Retries

Short answer: treat a video generation form as a capability-driven state machine, then make submission and status updates idempotent. Hide controls the selected capability cannot honor, persist a job before leaving the request path, and show a truthful state until the output has passed media validation.

That decision matters in a property-management media library. An agent may select a 16:9 apartment tour, ask for a short social cut, or upload a clip from a phone that arrives as a format the browser can preview but the worker cannot encode. A form that looks available when the backend will reject it creates support tickets. A status badge that says “done” before the file is playable creates a worse one.

How should a video generation UI gate capabilities and asynchronous status?

Start with a capability document, not a collection of disabled buttons. The document can describe accepted input containers, output formats, maximum duration, aspect ratios, audio policy, and whether a feature is available for the current account or queue. Keep the shape boring and explicit. The UI renders from it; the worker validates against the same contract.

For example, a capability response might say that an image-to-video operation accepts JPEG and PNG stills, supports 1:1 and 16:9 output, and returns MP4. The form should update its file picker, duration control, and aspect-ratio choices when the operation changes. If a capability is absent, remove the control or explain the boundary next to it. Do not let a stale browser bundle invent support.

The submit path needs a client-generated idempotency key. Store the key with the job record before enqueueing work. A retry with the same key should return the original job, not create a second video. This is the difference between a user seeing one “balcony tour” and a library receiving four copies after a Wi-Fi hiccup.

Keep the first response small: job identifier, accepted capabilities, and an initial state such as queued. The dashboard can then poll or subscribe to state changes. The request that creates work should never wait for encoding.

The state machine is the product contract

Use a finite set of states and document the transitions. A practical sequence is queued -> running -> validating -> succeeded, with failed and cancelled as terminal branches. The UI should render a state it received, not infer completion from elapsed time or the presence of a temporary object key.

One sentence is enough for the operator when a job is queued.

“Waiting for an available worker.”

When a worker finishes encoding, validation still has work to do. Check that the container is readable, the declared duration is within the requested limit, the video stream has a supported codec, and the generated asset can be fetched by the library service. Browsers and editing tools disagree about what they can decode; the media format guidance from MDN is a useful baseline, not a promise that every device will play every file.

The status endpoint should be safe to call repeatedly. Return a stable state, progress only when it is meaningful, and a machine-readable reason for terminal failure. Avoid leaking implementation details such as queue names. An operator needs “input format is not accepted” more than “worker 7 exited with code 137.”

Here is a compact Go model for rendering and persisting those transitions. The repository layer is deliberately an interface so the same rules can be tested without a queue.

package jobs

import (
    "context"
    "errors"
)

type State string

const (
    Queued     State = "queued"
    Running    State = "running"
    Validating State = "validating"
    Succeeded  State = "succeeded"
    Failed     State = "failed"
    Cancelled  State = "cancelled"
)

type Job struct {
    ID             string
    IdempotencyKey string
    State          State
    FailureReason  string
}

type Store interface {
    FindByIdempotencyKey(ctx context.Context, key string) (Job, bool, error)
    InsertQueued(ctx context.Context, job Job) error
}

func Create(ctx context.Context, store Store, key, id string) (Job, error) {
    if key == "" || id == "" {
        return Job{}, errors.New("id and idempotency key are required")
    }
    if existing, found, err := store.FindByIdempotencyKey(ctx, key); err != nil {
        return Job{}, err
    } else if found {
        return existing, nil
    }

    job := Job{ID: id, IdempotencyKey: key, State: Queued}
    if err := store.InsertQueued(ctx, job); err != nil {
        // A concurrent retry should read the committed job and reuse it.
        if existing, found, lookupErr := store.FindByIdempotencyKey(ctx, key); lookupErr == nil && found {
            return existing, nil
        }
        return Job{}, err
    }
    return job, nil
}
Enter fullscreen mode Exit fullscreen mode

The unique constraint behind InsertQueued is important. Checking first and inserting later is not enough under concurrent requests; the database must enforce one key per job. That small detail has saved more incident reviews than a clever progress animation ever did.

What should the form do when capability data changes?

Treat capability changes like configuration changes, not like validation errors to hide. On every operation change, fetch or select a versioned capability document and revalidate the current draft. If the selected aspect ratio disappears, keep the user’s other inputs and ask for a new choice. If a newly selected duration exceeds the limit, show the limit beside the field before submission.

Do not silently transcode an input just because a browser preview worked. Transcoding can alter quality, audio sync, and bandwidth. In a property library, the original clip may be the evidence an owner needs later. Offer an explicit normalization step when it is useful, and record that a derived asset was created.

Quality and bandwidth pull in opposite directions. A high-resolution source gives a better editing baseline, but uploading it over a mobile connection can delay a listing workflow. Let the operator choose a quality profile when the capability says it is supported, then show an estimate as an estimate. I’m not sure a single byte-based threshold works across every building and carrier; measure actual upload completion and revise the profile defaults from those observations.

Verification, retries, and rollback

Test the seams, because that is where the pager rings. Submit the same idempotency key twice, refresh during running, receive an out-of-order status event, and reopen a succeeded job on a device that cannot decode the output. The expected result is one job, a monotonic terminal state, and a clear recovery action.

A useful verification table looks like this:

Check Expected behavior Operator action
Capability removed mid-draft Form marks the field invalid and preserves other inputs Choose a supported value
Network retry after submit Same job identifier is returned Continue watching that job
Worker restart Job remains queued or resumes safely No duplicate submission
Validation failure State is failed with a user-safe reason Fix input or retry with a new key
Cancel request races completion One terminal state wins and is shown consistently Use the recorded result

For rollback, keep the generated asset separate from the library’s published pointer until validation succeeds. If a deployment changes the capability schema, accept the previous schema for existing jobs and stop creating new jobs with it. Roll back the producer and consumer together when possible; otherwise, a worker can interpret a field differently from the form that created it.

Observe four signals: submission-to-queue latency, queue age, time in each state, and validation failure rate by input format. Add a trace or correlation identifier to every transition. These measurements distinguish a slow encoder from a UI that is polling too aggressively, and they give you a defensible basis for changing quality defaults.

The catch is that this design is not suitable when users need instant, frame-accurate previews inside a live editor. There, a local preview pipeline and a separate export job are a better split. Stick with a simpler synchronous request for tiny, deterministic transforms where the work completes within your request timeout; adding a queue would only add operational surface.

References

Top comments (0)