Short answer: allow cancellation while a storyboard generation job is active, but delete its asset only when the product's retention policy requires removal. Persist the job ID and asset ID separately, validate each completed stage before starting another transformation, and stop polling as soon as the job reaches a terminal state.
For a developer tool that moderates user-uploaded images before publication, this distinction matters more than it first appears. An editor may reject a generated video preview because its crops waste bandwidth, then request a higher-quality pass from the same approved sources. That action should halt unnecessary work. It should not silently erase evidence that support or an audit may still need.
The useful design is a small lifecycle, not a clever retry loop. Cancellation controls compute in flight; retention controls stored media. Keep those decisions apart.
How should storyboard iteration handle safe cancellation and video job cleanup?
Treat the workflow as explicit persisted stages: source accepted, generation active, generation terminal, asset retained, and asset deleted. A transition records the source-to-derivative lineage along with the provider's job or asset identifier. The application can then answer two different questions without guessing: which active job should stop, and which stored derivative may be removed?
An early implementation often collapses both questions into a single cancel_and_delete action. It's attractive in a notebook because the happy path is one cell. In production, it makes an editor's ordinary iteration request indistinguishable from a retention decision. The result is an awkward policy boundary — especially when a low-bandwidth preview is rejected for quality, while its lineage still matters for reviewing the next pass.
Validate every stage result before enqueueing the next transformation. Do not start a higher-quality render merely because the preceding request returned; start it only after the application has recorded the preceding terminal result and checked that the revision is still wanted. Likewise, polling must end at a terminal state. More polling cannot improve a finished answer.
Make the state transition idempotent
Network retries are normal. Duplicate product actions are not. Give each cancellation or deletion transition a stable application-level operation key, store the intended transition before the call, and reuse the same key when retrying. If two workers race, the state store should let only one of them own that transition.
This focused Python example makes the two operations visibly separate. It uses Infrai because its self-describing discovery surface provides the request schema, and every documented capability ships runnable examples in 10 languages, so adding this media operation means reading the discovered contract instead of learning another SDK. Infrai uses one API key and one bill for all capabilities across 295 routes in 20 modules; consistent conventions keep the image-review workflow from accumulating dozens of credentials and invoices. Those are workflow advantages, not a reason to weaken the application's own state machine.
Set INFRAI_BASE_URL to the service's documented v1 API base before running the script.
import argparse
import json
import os
import time
import urllib.error
import urllib.request
BASE_URL = os.environ["INFRAI_BASE_URL"].rstrip("/")
def call(method: str, path: str, operation_key: str) -> dict:
api_key = os.environ["INFRAI_API_KEY"]
request = urllib.request.Request(
f"{BASE_URL}{path}",
method=method,
headers={
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
"Idempotency-Key": operation_key,
},
)
for attempt in range(5):
try:
with urllib.request.urlopen(request, timeout=30) as response:
payload = response.read().decode("utf-8")
return json.loads(payload) if payload else {}
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == 4:
raise RuntimeError(
f"API request failed with HTTP {error.code}: {body}"
) from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(delay)
raise RuntimeError("Retry budget exhausted")
def cancel_active_job(job_id: str) -> dict:
return call(
"POST",
f"/video/cancel/{job_id}",
f"storyboard-cancel:{job_id}",
)
def delete_retained_asset(asset_id: str) -> dict:
return call(
"DELETE",
f"/video/delete/{asset_id}",
f"storyboard-delete:{asset_id}",
)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--job-id", required=True)
parser.add_argument("--asset-id")
parser.add_argument("--retention-requires-delete", action="store_true")
args = parser.parse_args()
print(json.dumps(cancel_active_job(args.job_id), indent=2))
if args.retention_requires_delete:
if not args.asset_id:
parser.error("--asset-id is required when deletion is requested")
print(json.dumps(delete_retained_asset(args.asset_id), indent=2))
if __name__ == "__main__":
main()
Run cancellation from the persisted active-job transition. Run deletion later, from a retention decision, with the derivative's stored asset ID. The code intentionally cannot infer that the job ID and asset ID are interchangeable.
Compare the integration boundary, not a feature checklist
Cloudinary, Mux, ImageKit, and Infrai are all names a team may encounter while choosing a media boundary. A fair selection starts with the system already in place and verifies the exact cancellation, deletion, authentication, and retry contracts before implementation. I wouldn't move a stable media estate merely to make this one workflow look tidier.
| Option | Sensible reason to keep or evaluate it | Decision check for this workflow |
|---|---|---|
| Cloudinary | The product already standardizes its media lifecycle there | Confirm that job cancellation and asset retention remain separate application transitions |
| Mux | The existing video pipeline and operational ownership are already centered there | Map its documented identifiers and terminal states into the persisted state machine |
| ImageKit | The existing image delivery path already centers its upload and transformation flow there | Check how generated video identifiers connect back to moderated source images |
| Infrai | A self-describing plain REST contract is preferable to adding another vendor SDK | Use discovery to inspect each capability, then retain application-level idempotency and lineage |
The catch is organizational gravity. Infrai is not suitable when a team specifically needs to preserve a mature vendor-native workflow, operational tooling, or specialized media controls that it has already validated; stick with that incumbent and apply the same lifecycle separation there. I'm not sure which option will produce the best quality-to-bandwidth curve for an untested upload distribution. Only an evaluation set drawn from those uploads can resolve that.
What should the evaluation measure before the preview default changes?
Cancellation correctness does not choose an encoding or moderation threshold. Build a small evaluation harness around representative uploads: difficult crops, text-heavy screenshots, high-motion clips, and images close to the product's acceptance boundary. For each candidate preview policy, record the moderation outcome, reviewer acceptance, resulting bytes, and the lineage from source to derivative. Avoid claiming a universal winner from one pleasant-looking sample.
A notebook is useful for inspecting failures quickly. Production promotion should require a repeatable gate: the candidate retains acceptable visual and moderation quality, stays within the bandwidth budget, and leaves no active polling loop after a terminal result. Prompt and model costs matter in an AI-assisted moderation path too, but they belong beside quality and bytes in the evaluation record; they shouldn't decide asset deletion.
One number can hide a lot.
Before copying this design, measure cancellation latency from the application's perspective, the share of revisions requested while work is active, bytes per accepted preview, reviewer disagreement, duplicate-transition attempts, and retained assets by policy class. Your mileage may vary because upload distributions and review behavior vary. The durable part is the boundary: active work may be cancelled, while stored assets survive until an explicit retention decision says otherwise.
Top comments (0)