DEV Community

HoldenFox8476
HoldenFox8476

Posted on

Video Generation Capability Checks — Before Offering Express Users Unsupported Options

Use upload-time background removal only when every accepted source can be decoded, the transformation can finish outside the request path, and the original is retained. Otherwise, store the original and generate a derivative on demand behind a capability check. That decision rule applies equally to promo video generation: an Express or Node.js application should check the source asset and requested output contract before offering options to users. It keeps a logistics catalog honest while a temporary processor failure remains separate from the source of record.

Short answer: capability is a property of the asset, operation, and output contract together. Do not expose a generic remove background switch because a service happens to be configured. Inspect the upload, record normalized metadata, probe the selected processor with that metadata, and return an option only when the full contract is satisfiable.

How should Node.js check video generation capabilities before offering options?

The concrete job is mundane but unforgiving. A logistics operator uploads product photos for a catalog, and the system produces transparent-background derivatives for listing pages, labels, or marketplace feeds before those assets enter a promo video workflow. The architectural choice is when that work runs: immediately after upload, or only when a consumer asks for the derivative. Although the HTTP layer may be Express on Node.js, capability discovery does not belong in a framework-specific conditional. Put it behind a narrow domain interface so the route can ask one question and render the answer without knowing decoder or processor details.

The invariant is that the original object remains immutable. A background-removal result is a derivative with its own content type, dimensions, processor revision, and lifecycle. Replacing the original throws away evidence needed for reprocessing and makes a bad mask much harder to unwind. I would reject that design in the ADR even if it made the first implementation shorter: the lost rollback path is a bad exchange for a few fewer storage keys.

Three more invariants matter. First, advertised options must come from a capability decision made for the specific asset, not from a static feature flag. Second, the output contract must name a format that can represent the required result; transparency is not an abstract promise. MDN's image format guide documents that JPEG does not support alpha transparency, while PNG and WebP do. Third, retries must not create conflicting derivatives. A stable job key derived from the source identity and transformation specification gives the worker an idempotency boundary.

The failure boundary should also be explicit. Upload acceptance ends after the original is durably stored and its basic metadata is recorded. Transformation failure belongs to the derivative job. This separation matters in the same way an OTP send differs from account creation: accepting the primary action and completing a downstream delivery are related events, but pretending they are one transaction creates awkward retries and misleading user states.

Invariants and failure boundaries

Treat file extension, declared MIME type, and decoded content as three different signals. An object named crate.jpg can arrive with an incorrect Content-Type; trusting either string before decoding lets unsupported or malformed data reach expensive processing. The decoder result should supply width, height, frame count where applicable, and the actual media type. Limits on byte size, pixel count, and processing time belong before the transformation queue.

Stop there.

Metadata is evidence, not permission.

A useful capability record is deliberately small: source media type, dimensions, alpha presence, operation name, requested output type, and the processor contract revision used for the decision. Avoid copying arbitrary embedded metadata into logs. Product photos can contain camera and location fields that have no operational value, and retaining them expands the compliance surface without improving the mask.

There are two independent rejection classes. A permanent rejection means the asset cannot satisfy the contract, such as requesting transparent JPEG output. A transient rejection means the operation is valid but capacity or a dependency is temporarily unavailable. The UI may hide a permanently impossible choice, but it should represent transient unavailability as retryable rather than quietly changing the requested format. Spam-filter work teaches the same lesson: policy rejection and delivery delay demand different recovery paths.

That distinction matters.

Decision point Process after upload Process on demand
First derivative latency Paid before a consumer asks Paid on the first request
Work for unused assets Always incurred Avoided
Upload request Keep asynchronous; return after original storage Unchanged by transformation
Failure visibility Job state is available before consumption Consumer encounters pending or failed state
Cache strategy Derivative can be pre-populated Derivative must use a stable cache key
Best fit Most accepted photos need the same derivative Demand is sparse or output contracts vary

Neither column wins universally. My first instinct for a catalog is upload-time generation because it makes downstream video assembly pleasantly boring. The correction comes from looking at demand, not taste: if only a small subset of shipment photos becomes promotional material, eager processing manufactures unused derivatives. In a feed where nearly every SKU needs one transparent PNG, an asynchronous upload-time job buys predictable readiness. For archival shipment photos that are rarely published, on-demand work avoids creating derivatives nobody reads. The traffic shape resolves the choice; the mere presence of a processing API does not. This is an explicit trade-off between readiness and wasted work, not a universal rule.

Put capability discovery before option presentation

The capability endpoint should be a pure decision layer over normalized asset metadata and a processor adapter. It should not launch a job. Keeping those actions separate prevents a page refresh from consuming capacity and makes authorization easier to reason about. In an Express application, the route handler can translate the domain result into JSON, but the check itself should remain callable from a queue consumer and a batch audit as well. That boundary is why the example below is Python rather than Node.js: it shows the domain contract without turning the article into a framework setup guide, and the same inputs and outputs can be implemented behind an Express route.

The following Python sketch shows the critical path. The types are narrow on purpose, and the rules are local even if an adapter later supplies additional constraints. probe represents a generic interface, not a network route tied to a product.

from dataclasses import dataclass
from typing import Protocol


@dataclass(frozen=True)
class AssetFacts:
    asset_id: str
    media_type: str
    width: int
    height: int
    decoded: bool


@dataclass(frozen=True)
class TransformSpec:
    operation: str
    output_type: str
    contract_revision: str


class Processor(Protocol):
    def probe(self, asset: AssetFacts, spec: TransformSpec) -> tuple[bool, str]: ...


def removal_option(asset: AssetFacts, processor: Processor) -> dict:
    spec = TransformSpec(
        operation="background_removal",
        output_type="image/png",
        contract_revision="2026-09",
    )

    if not asset.decoded:
        return {"available": False, "reason": "source_not_decoded"}
    if asset.media_type not in {"image/jpeg", "image/png", "image/webp"}:
        return {"available": False, "reason": "unsupported_source_type"}
    if asset.width < 1 or asset.height < 1:
        return {"available": False, "reason": "invalid_dimensions"}

    supported, reason = processor.probe(asset, spec)
    return {
        "available": supported,
        "reason": reason,
        "operation": spec.operation,
        "output_type": spec.output_type,
        "contract_revision": spec.contract_revision,
    }
Enter fullscreen mode Exit fullscreen mode

The response includes a reason code because false is operationally weak. A client needs to distinguish a source it can replace from a capability it may retry later. Keep those codes stable and finite; do not pass through a processor's raw error message. Raw messages change, may leak implementation details, and are poor analytics dimensions.

Once the user requests an available option, enqueue a job whose identity is deterministic. For example, hash the immutable source object version plus a canonical serialization of the transform specification. The worker writes to a versioned derivative key, then atomically records completion. Two deliveries of the same queue message should converge on the same artifact. Exactly-once execution is unnecessary when the effect is idempotent.

No guesswork.

Operational checks that prevent false promises

Test the decision table before testing image quality. Contract tests should cover a valid JPEG source to PNG output, a malformed image with a plausible extension, zero or excessive dimensions, an unsupported decoded type, a permanent processor rejection, and a transient probe failure. The UI contract must prove that a permanent rejection suppresses the option while a transient state remains explainable and retryable.

Then test the media path with a reviewed fixture set: centered products, thin handles, reflective packaging, white objects on white backgrounds, shadows that should remain, and images that already contain transparency. Quality evaluation needs human-approved masks or an agreed review rubric; a capability probe can establish that processing is possible, not that the result is acceptable.

Observe each stage with bounded labels. Useful counters include capability decisions by reason code, jobs accepted, jobs completed, permanent failures, transient failures, and cache hits. Record duration and queue age as distributions. Do not put asset IDs, filenames, or raw exception text into metric labels; cardinality grows quickly, and filenames may carry customer data. Correlation IDs can live in access-controlled logs with an explicit retention period.

Rollout should begin with decision-only shadowing. Compute capability results without showing the option, compare them with offline fixtures, and inspect rejection distributions. Next, expose the option to a limited catalog segment while keeping originals and a kill switch for job creation. A processor revision belongs in both the capability record and derivative key, so rollback selects an older known contract rather than overwriting artifacts in place.

Cost still belongs in the ADR, but it is not the headline. Measure decoded megapixels, queue time, processor time, storage bytes, and derivative reads. Those quantities let the team compare eager waste against on-demand latency without depending on a changing price sheet. I would revisit the decision when those measurements show that the assumed demand shape was wrong; an ADR is a recorded bet with a review trigger, not permanent doctrine.

The rejected option, and when it becomes right

For a mixed logistics catalog, reject synchronous transformation inside the upload request. Its failure boundary is wrong: a slow or unavailable transformation can make a perfectly valid original appear to have failed upload, encouraging users to retry and creating duplicate objects. It also couples request timeouts to media complexity.

There is a valid narrow case. A controlled internal tool that accepts small, homogeneous images and requires the transformed preview before an operator can continue may choose synchronous processing. The contract must cap decoded size and duration, preserve the original first, and return an explicit failure without pretending the source disappeared. Even there, capability discovery should precede the action.

The final decision is therefore conditional. Choose asynchronous upload-time generation when the derivative is near-universal and readiness matters; choose on demand when use is sparse or specifications vary. In both designs, gate the visible option on decoded asset facts, output-format semantics, and a versioned processor probe. Preserve the original, separate permanent from transient failure, and make derivative creation idempotent.

References

Top comments (0)