How it works
From source image to looping mp4 — the architecture in one page.
How it works
Motioness is an edge-hosted image-to-motion proxy. One URL replaces an <img src> and the response is either a poster jpg (immediate) or a looping mp4 (generated once, cached forever).
Request flow
textBrowser ──→ /v1/{projectKey}/{src}.{fmt} │ ▼ Cloudflare Worker │ ┌──────────┴──────────┐ │ Already cached? │── yes ──→ Stream from edge cache (~5ms) │ │ └──────────┬──────────┘ │ no ▼ ensureAsset(srcUrl, opts) │ ┌──────────┴──────────┐ │ Asset row in D1? │── yes (pending) ──→ 202 + Retry-After │ │── yes (ready) ──→ stream from R2 └──────────┬──────────┘ │ no ▼ Insert asset row, enqueue: 1. Claude vision → motion prompt 2. Replicate i2v → mp4 (single OR two-stage loop) 3. R2 write → durable bytes 4. Webhook fire → asset.completed │ ▼ Return 202 to browser
The three pipelines
The mode parameter (or project default) picks one of three pipelines:
| Mode | Pipeline | Use case |
|---|---|---|
loop | Two-stage stitch — generate forward + return-to-start, merge in fixed timeline | Backgrounds, hero accents, ambient motion |
animate-in | Single i2v call, played once on entry | Reveal animations, build-ups, story beats |
animate-on-hover | Same as animate-in but client-paused until hover | Interactive cards, gallery items |
loop is two Replicate calls; animate-in and animate-on-hover are one. See motion modes.
Tier-based model selection
The image-to-video model is picked from the project owner's plan, not from the request:
| Tier | Model | Quality | Loop-capable |
|---|---|---|---|
| Free | wan-fast | low | no — uses crossfade |
| Pro ($19/mo) | seedance | standard | yes (native) |
| Team ($79/mo) | p-video | high | yes (native) |
Customers can't request a higher-tier model than their plan. Quality scales with the plan.
Edge cache + content-addressing
Asset IDs are content-addressed: same (src, motion, mode, dur, prompt, seed, idem, aspect) produces the same asset_id, which produces the same R2 path, which produces the same cached URL.
textasset_id = sha256(src_url || motion || mode || dur_seconds || prompt || seed || idem || aspect)[0:16]
This means:
- Two browsers hitting the same proxy URL never re-generate.
- Adding
?idem=variant-aand?idem=variant-bproduces two distinct assets from the same source. - Removing query params doesn't break cache — non-content params (
wait_ms,sig,exp) are stripped before hashing.
Storage
| Tier | Backend | What's there |
|---|---|---|
| 1 | Edge cache | mp4 + jpg responses, ~5ms hot reads |
| 2 | KV (CACHE) | asset:{id} index pointing at R2 path — skips D1 on cache miss |
| 3 | R2 (R2_ASSETS) | Durable mp4s (assets/{id}.mp4), posters (posters/{srcHash}.jpg) |
| 4 | D1 (DB) | Rows for assets, generations, asset_events, webhook_deliveries, projects, users |
Reliability surface
Three independent recovery mechanisms keep generation honest:
- Per-stage retry envelope — transient errors (Replicate 5xx/429, R2 5xx, timeouts) retry up to 3 attempts per stage with
[0s, 1s, 4s]backoff. - Replicate webhook + cron reconciler — if the webhook never lands, a cron job runs every minute and queries Replicate directly to finalize stuck assets after 5 minutes. After 10 minutes it marks them
failedwithpipeline_stalled. - Standard Webhooks delivery — outbound webhooks retry on
30s, 2m, 10m, 30m, 2hand dead-letter after 5 attempts.
You can re-enqueue a dead delivery from the dashboard or via POST /api/projects/:id/webhooks/deliveries/:deliveryId/redeliver.
Brand context
When configured, a project's brand context (colors, fonts, tagline, content inventory) is injected into the vision prompt. The vision model gets:
textBrand: Acme — fast SaaS observabilityTone: technical, confidentColors: #0d9373 (primary), #f59e0b (accent), dark schemeFonts: Inter / IBM Plex Mono
This biases motion choices toward what fits the brand — subtle for editorial sites, kinetic for consumer brands, etc. See brand context for how to populate it (manual fields, brand-detect, or deep-crawl).
What you don't have to think about
- Prompt engineering — the vision model writes the prompt for you.
- Model selection — tier picks it;
quality=is a hint when you want explicit control. - Aspect handling —
aspect=preserve(default) matches the source. - CDN / cache invalidation — content-addressed URLs never need invalidation.
- Format negotiation — request
.mp4for video,.jpgfor poster fallback.
What you should know
- First request to an mp4 returns
202while generation runs. Use?wait_ms=8000to long-poll. - Free-tier mp4s use
crossfadeloop mode (visible seam); Pro+ mp4s loop natively. - The proxy is read-only for the source URL — Motioness never modifies your source images.
- Generation costs apply to the project owner, not the embedding site.