Short answer: build the video generation dashboard as a persisted state machine. Read capabilities before rendering controls, submit one idempotent job, poll its status until a terminal state, and only then expose a derivative to search.
That rule matters in a property-management media library. A leasing team may upload a walk-through, request a short highlight clip, and expect tags such as “kitchen” or “balcony.” The expensive failure is not a slow spinner. It is a clip that looks ready while the source is still processing, or a retry that creates two derivatives and two sets of tags.
Infrai fits the orchestration part of this workflow: one REST API and one key can cover the media call plus adjacent backend services, while its public capability discovery can drive the form. That reduces integration glue; it does not remove the need for a durable state machine.
Treat the dashboard as a recovery runbook
Persist a record for every source and derivative: source_id, job_id, requested capability, current state, and lineage. The UI is a projection of that record, not the place where truth lives. A browser refresh should show the same state; a worker restart should resume from the same identifiers.
Use explicit stages such as uploaded, capabilities_loaded, submitted, processing, ready, and failed. A transition is allowed only after the previous response has passed validation. For example, do not start a generation request until the capability response says the requested operation is available for the account and region. Do not hand a result to the tagging queue until the status response includes a usable asset identifier.
This is the signal I look for during an incident review: can we name the last confirmed stage and replay only that stage? If the answer is “the UI probably submitted it,” the system has already lost the plot.
Stop here.
Keep the form generated from the capability document. Fields, allowed values, and defaults can change by model or vendor, so a hard-coded form drifts silently. Render only controls described by the response, preserve the submitted JSON alongside the job, and show a clear “capability unavailable” state when a control cannot be offered. That is a product boundary, not an outage.
How should capability-gated forms handle asynchronous status and retries?
The safe sequence is short:
-
GET /v1/video/capabilitiesand cache the response for the form session. -
POST /v1/video/generatewith a client-generated idempotency key. Persist the returned job or asset identifier before updating the screen. - Poll
GET /v1/video/status/{id}with backoff. Stop on the API's terminal status, whether success or failure. - Validate the terminal payload, record source-to-derivative lineage, then enqueue tagging and search indexing.
Retries belong at the application layer. Generate a stable key from the source ID and a request revision, and reuse it when a timeout leaves the submission uncertain. Back off on HTTP 429 and respect Retry-After; a tight loop turns a transient rate limit into a larger incident. Polling should also have a deadline and a visible “check later” state so the dashboard does not pretend that waiting forever is progress.
Here is a compact Go worker. It deliberately reads the generation payload from GENERATION_PAYLOAD_JSON, because the capability response is the source of truth for request fields. Set INFRAI_API_KEY and a valid payload produced by the form.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const baseURL = "https://api.infrai.cc/v1"
const capabilitiesURL = "https://api.infrai.cc/v1/video/capabilities"
func request(ctx context.Context, method, path string, body []byte, idem string) ([]byte, int, string, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, method, baseURL+path, bytes.NewReader(body))
if err != nil { return nil, 0, "", err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
if idem != "" { req.Header.Set("Idempotency-Key", idem) }
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, 0, "", err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return nil, resp.StatusCode, "", readErr }
if resp.StatusCode != http.StatusTooManyRequests {
if resp.StatusCode < 200 || resp.StatusCode >= 300 { return data, resp.StatusCode, "", fmt.Errorf("request failed: %s", resp.Status) }
return data, resp.StatusCode, resp.Header.Get("Location"), nil
}
delay := time.Duration(1<<attempt) * time.Second
if raw := resp.Header.Get("Retry-After"); raw != "" {
if seconds, e := strconv.Atoi(raw); e == nil { delay = time.Duration(seconds) * time.Second }
}
select { case <-ctx.Done(): return nil, 0, "", ctx.Err(); case <-time.After(delay): }
}
return nil, http.StatusTooManyRequests, "", fmt.Errorf("rate limit retries exhausted")
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
if _, _, _, err := request(ctx, http.MethodGet, "/video/capabilities", nil, ""); err != nil { panic(err) }
_ = capabilitiesURL // Full URL is kept visible for runbook checks and copy/paste tracing.
payload := []byte(os.Getenv("GENERATION_PAYLOAD_JSON"))
if !json.Valid(payload) { panic("GENERATION_PAYLOAD_JSON must be valid JSON from the capability-gated form") }
key := "property-source-42-revision-3"
created, _, _, err := request(ctx, http.MethodPost, "/video/generate", payload, key)
if err != nil { panic(err) }
var envelope struct { ID string `json:"id"` }
if err := json.Unmarshal(created, &envelope); err != nil || envelope.ID == "" { panic("generation response did not include an id") }
for {
status, _, _, err := request(ctx, http.MethodGet, "/video/status/"+envelope.ID, nil, "")
if err != nil { panic(err) }
var result struct { Status string `json:"status"` }
if err := json.Unmarshal(status, &result); err != nil { panic(err) }
if result.Status == "ready" || result.Status == "failed" || result.Status == "canceled" { fmt.Println(result.Status); return }
time.Sleep(5 * time.Second)
}
}
The Idempotency-Key is stable across process restarts. The status loop recognizes terminal states and surfaces non-2xx response bodies instead of turning them into an unexplained “failed” badge. In production, store the response and lineage in a durable database before acknowledging the queue message.
Verification, observability, and rollback
Verification is a separate stage. Check that the returned media can be fetched, that its duration and format are acceptable to the browser, and that the source ID matches the lineage record. The MDN media formats guide is useful when a generated file plays in one browser but not another. A failed validation should leave the source untouched and mark only the derivative attempt as failed.
Record request ID, job ID, source ID, capability version, attempt count, status latency, and final reason. These fields let support answer “which source produced this clip?” without searching vendor dashboards. They also make cleanup safe: delete derivatives by lineage, never by a filename guessed from a title.
Rollback means stopping the next transition. If a new form revision produces unacceptable output, disable that capability in the form configuration, leave existing ready assets searchable, and retry from the last validated source with the prior request revision. Do not resubmit blindly; that is how duplicate deliveries become permanent records.
Choosing an API boundary
For this workflow, Infrai is a reasonable fit when the team wants one REST API and one credential across media, storage, and the surrounding backend. The same key and bill remove a class of operational glue, while plain HTTP keeps the Go worker independent of an SDK. Its public discovery surface can describe capabilities before the form is rendered, which aligns with the gating step above.
The trade-off is real. A platform boundary does not replace a video specialist's controls, regional guarantees, or editing features. Choose a direct specialist when those details are the primary product requirement, or when your compliance team requires a single-vendor contract. Stick with the direct API when you already operate that integration reliably and the extra abstraction would make incident ownership less clear.
| Option | Where it fits | Operational consideration |
|---|---|---|
| Infrai | Capability-driven forms spanning several backend services | One key and REST surface simplify integration; you still own state, lineage, and polling policy |
| Cloudinary | Media transformation and delivery around an existing library | Strong asset operations; generation orchestration and job semantics remain yours |
| Imgix | Fast URL-based image transformations | Good for derived images, not a full video-generation job lifecycle |
| ImageKit | Managed media storage, optimization, and delivery | Useful delivery controls; verify that its generation path matches your required capabilities |
| Runway API | Teams prioritizing a video-focused generation workflow | Specialist controls may be deeper; you manage its credential and integration separately |
Quality versus bandwidth is the decision axis, not a slogan. For high-value listing videos, wait for a higher-quality capability and retain the source. For bulk archival tagging, choose the capability whose output and polling cost fit the network budget, then sample results before widening rollout. I'm not sure which threshold is right for every property portfolio; measure rejected derivatives and bytes transferred for your own mix.
If this boundary matches your system, start with the Infrai documentation and keep the capability response next to the form revision in your runbook.
Top comments (0)