Install
openclaw skills install @ofoxai/product-demoRequires OFOX_API_KEY — create one at https://app.ofox.ai, plus two real screenshots of one interface in two states. Animates the transition between them as one video — both go into a single Ofox video job, and the model cross-fades the values that changed while the rest of the layout holds. Use when a user wants a short software demo animation out of captures they already have, e.g. "turn these two screenshots into a demo clip", "show the dashboard going from the free plan to the paid one", "animate this settings change for the docs", or "make a clip of the counter going from 3 to 25". Do not use for a pair that is not a user interface (see keyframe-animation), for a screen recording (record it instead), or when only one screenshot exists.
openclaw skills install @ofoxai/product-demoTwo screenshots of the same interface — before and after a state change — go into one Ofox video job as the first and last frame, and the model animates the change between them.
This skill is a thin, scenario-specific layer over
ofox-video-core. It owns how to capture a
usable pair, the prompt for a UI transition, and the pre-generation cost
estimate; ofox-video-core owns talking to the Ofox API correctly and safely
(the OFOX_API_KEY handling, the no-resubmit rule, error-code mapping,
download/verification, and reporting the downloaded file's absolute
VIDEO_PATH). Read that skill's safety contract before using this one —
it is not restated here.
The shared prompt craft is in
../ofox-video-core/references/prompt-structure.md,
the pre-prompt question rules in
../ofox-video-core/references/creative-brief.md,
and the spend rule in
../ofox-video-core/references/approval-gate.md.
This file carries only what is specific to a UI pair.
Resolve once, before the first call:
for d in ../ofox-video-core \
../ofoxai-skills-ofox-video-core \
~/.agents/skills/ofox-video-core \
~/.agents/skills/ofoxai-skills-ofox-video-core \
~/.claude/skills/ofox-video-core; do
[ -f "$d/references/ofox-video.sh" ] && echo "$d" && break
done
Examples below are written as ../ofox-video-core/... (the skills.sh /
ClawHub / npx ofox-skills layout, where a skill's directory is named after
the skill). If the probe found a different directory — LobeHub unpacks each
skill as ofoxai-skills-<name>, so the sibling there is
ofoxai-skills-ofox-video-core — substitute it, in the ofox-video.sh
commands and in the references/*.md links alike.
That ../ is relative to this skill's own directory, which is also where
the probe has to run: every example below assumes the working directory is the
one this SKILL.md sits in. From anywhere else nothing resolves — use the
absolute path the probe printed (candidates 3–5 are absolute already), or, in
a clone of this repo, skills/ofox-video-core/references/ofox-video.sh from
the repo root.
Nothing found → the core skill isn't installed; see "If the script isn't found".
Run this once per session (not on every request):
bash ../ofox-video-core/references/ofox-video.sh check
If it fails, follow ofox-video-core's guidance (install curl/jq, or get
an OFOX_API_KEY at https://app.ofox.ai) — don't dead-end the conversation,
and don't re-run this check on every subsequent request once it has passed.
check reports whether the key is present, not whether it is valid; it
makes no network call. The key is an ordinary environment variable in the
shell that runs the script — the script reads nothing from a .env file on
its own, so if a project keeps it in one it has to be sourced into the same
shell before the command runs.
A failing check is not a stop sign for this conversation. It exits 2
when OFOX_API_KEY is unset, but models, providers and
generate --dry-run all run without a key — so a keyless user can still get
the two captures read, the prompt written and the job priced, and only needs
the key at the moment they say yes to the cost table. Do that work first
rather than opening with a signup link; see "Pricing a job with no API key".
This scenario was expected to fail. Generative video models fabricate text —
every other scenario skill in this repo carries an AVOID: subtitles, on-screen text line for exactly that reason — and a user interface is almost
entirely text. The prediction was that the interpolated middle would turn the
UI's strings to mush and a demo built this way would be worthless.
It was tested, and the prediction was wrong. Job 5e59baa2,
bytedance/seedance-2.5, 4 seconds, 480p, billed 44 cents. The inputs
were two genuine Chrome-rendered screenshots of one pricing/usage panel,
differing in four places:
| First frame | Last frame | |
|---|---|---|
| plan name | Starter | Business |
| price | $29.00 | $99.00 |
| seat count | 3 | 25 |
| button label | Upgrade plan | Manage seats |
What the delivered clip's own frames show:
Why the pessimistic reasoning failed, and this is the load-bearing part:
the repo's AVOID: on-screen text lines exist for text the model has to
invent. With both endpoints supplied, it is interpolating between two
given renderings instead — a different task with a different failure
profile. Copying that AVOID line into this skill would be cargo-culting a rule
whose premise does not hold here, so this skill does not carry one; see
"The prompt" below.
Job 427278b4 (2026-09-15), same model, duration, tier and input pair, again
44 cents, with the prompt filled in from this file's own template by an
agent working from this skill and nothing else — so what that run tested was
the skill rather than the API. Read off the delivered clip's final frame: all
four changed values render correctly and legibly — Business, $99.00,
25, Manage seats — the layout is identical to the input capture, nothing
drifted, and no text was invented.
Same pair, so it widens nothing about the inputs. What it adds is that the template in this file, not a hand-written prompt, produces the result, and that the result replicated rather than being one lucky roll — which matters more here than in most scenarios, because a seed does not reproduce a take on this API, so a second run really is a second sample.
bytedance/seedance-2.5. The catalog lists
i2v for the current video models, but i2v says nothing about locking
both ends at once — that granularity is not in the catalog and has not
been tested here.The same two-frame mechanism was measured for motion in the same session
(job 259c3ce2, a moving object rather than a UI): the change is
front-loaded, covering about 53% of the distance in the first quarter of
the clip and arriving at three seconds of four, then holding. The UI job is
consistent with that shape — crisp and settled by t=2.0s of a 4-second clip —
but the UI clip was read for legibility rather than sampled for an easing
curve, so treat "it settles early and then holds" as the pattern to plan for,
measured directly on the motion clip and observed to be compatible here.
Practical consequence: the tail of the clip is a hold on the final state. That is usually what a docs GIF wants anyway. Budget duration knowing the last beat is stillness, and don't plan for the change to land on the final frame.
The shared rules — the three tiers, one round of at most four questions, the
"Let the AI decide" discipline, the order with the approval gate — are in
../ofox-video-core/references/creative-brief.md.
This section adds only this scenario's question set.
Almost every axis is settled by the two screenshots: the subject, the layout, the colours, the framing and the output's shape are all in the pair. Zero questions is the common case when both files arrive with names that say which is which.
| Tier | Product-demo axes |
|---|---|
| must-ask | does the second screenshot exist? and, when the input leaves it ambiguous, which state comes first. Neither can be guessed: a capture that does not exist cannot be invented, and the wrong order produces a clip that runs backwards and bills the same |
| ask-if-open | whether anything should be emphasised during the change, when the request hints at one — and the honest answer is usually "a plain cross-fade", because that is the only behaviour with a run behind it |
| never-ask | duration, resolution, model, provider, audio (off by default here). Aspect ratio is not a question at all: with frames attached the clip follows the screenshots. Two durations or two resolutions are two rows in the cost table |
| # | Tier | header | Question | Options (1 = recommended) | Ask when |
|---|---|---|---|---|---|
| 1 | must-ask | Screens | I need both ends: a capture of the interface before the change and one after it. Do you have the second one? | Yes — I'll give a local path (recommended): both go into one job and the clip lands on both exactly. / No, I can capture it: take it now, same window size and same scroll position as the first. / No, and I can't: then there is nothing to interpolate towards — a screen recording, or a single-frame clip from another skill, is the honest alternative. No AI option — a screenshot of software that was never captured cannot be invented, and a made-up interface is not a demo of yours. | Only one capture is attached and the user did not name a second. |
| 2 | must-ask | Order | Which capture is the starting state? | the two filenames as the two options. No AI option. | Two images arrived and nothing in the names, the paths or the message says which comes first. Skip it whenever the names themselves state an order — before/after, a/b, 1/2, 01/02, old/new, free/paid, off/on, light/dark, two version numbers or two capture timestamps — or the message says so. The list is illustrative: any pair that reads as a sequence to you will read that way to the user. |
| 3 | ask-if-open | Emphasis | Should the clip do anything beyond changing the values? | No — a plain cross-fade of what differs (recommended): the only behaviour measured here, and it kept every string legible. / Yes, describe it: it goes in the prompt as a beat — untested, and anything the model has to add that is in neither capture is exactly the kind of invention this mechanism avoids. I will say so in the cost table. / Let the AI decide — which in practice means the first option. | The request hints at a highlight, a pointer, a zoom or a callout. Skip it otherwise. |
Not asked: duration and resolution (rows in the cost table), the model, the provider, the audio flag, the aspect ratio.
On top of the generic rows in creative-brief.md:
| Input says… | Axis | Value |
|---|---|---|
filenames that state an order — before/after, a/b, 1/2, 01/02, old/new, light/dark, free/pro, off/on, version numbers, capture timestamps | Order | as named; don't ask. Treat these as examples of the pattern, not as the whole list |
| "from this state to that one", in attachment order | Order | attachment order |
| "for the docs", "for the changelog", "a GIF" | duration | the model's minimum — ofox-video.sh models prints each model's range |
| "no music", "silent" | audio | off, which is already the default here |
| Answer | Lands in |
|---|---|
| Screens: two paths | --frame-first-image A and --frame-last-image B |
| Order | which path goes on which of those two flags |
| The four (or however many) differences you read off the pair | the CHANGE line, quoted verbatim |
| Emphasis | an extra beat in the prompt, or nothing |
| Duration | --duration, and a row in the cost table |
Brief:
- Start state: /Users/me/caps/plan-starter.png (given)
- End state: /Users/me/caps/plan-business.png (given)
- What differs: plan name, price, seat count, button label — 4 changes
- Emphasis: none, plain cross-fade (the measured behaviour)
- Duration / resolution / audio: 4s / 480p / off — one row below; 720p as a second row
- Frame shape: 1440x900 on both files, so the clip comes out in that shape
Then the full prompt, then the cost table, all in one message.
The pair is the whole job. Everything the model is asked to do lives in the difference between the two files, so a bounded difference is the entire craft here.
sips -g pixelWidth -g pixelHeight <file>,
identify <file>) rather than trusting a filename or an export dialog —
this repo has more than one record of a reported size disagreeing with the
file actually on disk.bytedance/seedance-2.5 refuses a photoreal-person reference at submission
(input_moderation_failed, nothing billed) — a documented ofox-video-core
behaviour. An avatar illustration is a different case and has not been
tested here either way.ofox-video-core base64-encodes a local readable file into the request; a
publicly reachable URL has been rejected upstream at least once in this repo.The prompt's job here is smaller than in any other Seedance scenario: both endpoints are supplied and the model can see them. It has to name what changed so the cross-fade lands on the right elements, and say that everything else holds still.
Vocabulary and the general formula are not repeated here — see
../ofox-video-core/references/prompt-structure.md.
Two things from that file bear on this template: negative clauses are honoured
more reliably than positive ones, and a camera verb on its own tends not to
produce a camera move — which is why the VIEW line below asks the frame to
stay put rather than asking it to travel.
SCREEN: the first frame and the last frame are two screenshots of the same <app or page name>, captured at the same window size and the same scroll position. Every element keeps the position, size, typeface, weight and colour it has in those two images.
CHANGE: between them, <element> reads "<value in the first frame>" and becomes "<value in the last frame>"[; <element 2> goes from "<A>" to "<B>"; …]. Those are the only things that change; every other label, number, icon and control stays exactly as it is.
VIEW: the view is locked — the screen is shown flat and straight on, framed exactly as it is in the two screenshots, at the same scale and in the same position. The framing does not move, scale, rotate or tilt at any point, and the page does not scroll.
ENDING: the clip settles on the second screenshot and holds it.
AVOID: a cut, a fade to black, a dissolve of the whole screen, a wipe or a slide transition; camera shake, zoom, pan or reframing; new panels, menus, dialogs, tooltips, toasts or badges appearing; a mouse pointer or cursor appearing; the layout shifting, reflowing or resizing; letters or digits becoming blurred, doubled or malformed.
The VIEW line says "framed exactly as it is in the two screenshots", not "the interface fills the frame". Whatever the pair shows — a full window, a panel with room around it, a cropped detail — is already the framing, and it is the two attached images that decide it. Telling the model the interface fills the frame is a description only when it happens to be true; on a capture with page around the panel it reads as an instruction to re-frame, which means inventing what sits outside the images it was given. Say what the pair is framed like, or say nothing about it. If a tighter crop is wanted, crop both captures before generating, exactly as with the aspect ratio.
There is deliberately no blanket "no on-screen text" item in that AVOID list, and it is the one line not to copy in from the other scenario skills. Those lines exist for text the model has to invent. Here both endpoints are supplied, so it is interpolating between two given renderings — the measured run kept every string correctly spelled and correctly positioned throughout — and a blanket prohibition on text would be pointed at the user's own interface. What this list does forbid is text-shaped things that are in neither capture (a tooltip, a toast, a badge) and text that stops being text (blurred, doubled, malformed glyphs), which are the real failure modes.
The measured pair: one pricing panel, four values changed.
SCREEN: the first frame and the last frame are two screenshots of the same pricing and usage panel, captured at the same window size and the same scroll position. Every element keeps the position, size, typeface, weight and colour it has in those two images.
CHANGE: between them, the plan name reads "Starter" and becomes "Business"; the price reads "$29.00" and becomes "$99.00"; the seat count reads "3" and becomes "25"; the button label reads "Upgrade plan" and becomes "Manage seats". Those are the only things that change; every other label, number, icon and control stays exactly as it is.
VIEW: the view is locked — the screen is shown flat and straight on, framed exactly as it is in the two screenshots, at the same scale and in the same position. The framing does not move, scale, rotate or tilt at any point, and the page does not scroll.
ENDING: the clip settles on the second screenshot and holds it.
AVOID: a cut, a fade to black, a dissolve of the whole screen, a wipe or a slide transition; camera shake, zoom, pan or reframing; new panels, menus, dialogs, tooltips, toasts or badges appearing; a mouse pointer or cursor appearing; the layout shifting, reflowing or resizing; letters or digits becoming blurred, doubled or malformed.
Quote the values verbatim, in quotes, exactly as they appear — including the currency symbol and the decimals. They are strings that exist in both captures; retyping them approximately in the prompt is the one way to introduce a discrepancy into a job that otherwise has none.
Read the artifact, not the status. STATUS completed says nothing about
legibility. The check that produced the evidence above — run on both jobs — and the one to repeat:
# the delivered clip's own first and last frames, against the two inputs
ffmpeg -i out.mp4 -frames:v 1 first.png
ffmpeg -sseof -0.1 -i out.mp4 -frames:v 1 last.png
# and the middle, where the cross-fade lives
ffmpeg -ss 1.3 -i out.mp4 -frames:v 1 mid.png
Look at three things: are the changed values spelled correctly and in the right place at the mid-point; do the unchanged labels stay solid; has anything moved. If ffmpeg isn't installed, open the file and scrub — the point is that a human or an agent looks at frames, not that a particular tool is used.
With a frame attached, the output's aspect ratio follows the frame, and how it gets there depends on the model:
bytedance/seedance-2.5, ofox-video-core forces
aspect_ratio=adaptive and prints a NOTE: saying so, because the API
requires it for image-to-video on that model. Anything you passed is
overridden;adaptive, the script defaults to
it when you pass no --aspect-ratio, and keeps your value if you pass one;adaptive, the script sends no ratio and
says so — the frames' shape may not survive.So: don't pass --aspect-ratio on this route, on any model. Crop both
screenshots to the shape you want before generating; that crop is what decides
the output. A browser window is rarely 16:9, and a padded frame bakes the bars
into the video — crop instead. Relay the NOTE: the script prints; it names
which branch applied.
This needs ofox-video-core 1.22.0 or newer. On an older core, a non-Seedance
model with frames attached sent no ratio at all, with nothing printed to say
so.
| Parameter | Default | Why |
|---|---|---|
--frame-first-image / --frame-last-image | both, in one generate call | the mechanism this skill is built on; both measured jobs honoured both ends |
--model | not passed — the script's own default applies, unless the user named a model, which always wins | both measured jobs ran on bytedance/seedance-2.5; whether both ends can be locked on another model is untested here. models lists the models but does not mark which one the script defaults to — the MODEL line a generate --dry-run prints is what names the id that will really be sent, which is also the id the cost table has to carry |
--duration | the model's minimum for a single bounded state change | the measured clip was crisp and settled by t=2.0s of 4 seconds; extra seconds buy hold and bill per second. ofox-video.sh models prints each model's range |
--resolution | the cheapest tier for the draft, then re-render the chosen seed higher. When the user named neither — the common case — draft at the model's cheapest tier and put the next tier up as a second row of the same cost table, so the upgrade is priced rather than asked about | both measured jobs ran at 480p, the cheapest tier their model lists, and the text stayed legible both times at that resolution, on that pair — fine lettering at a small tier is the first thing to check on a draft, and the second row is what the user reaches for when it fails that check. models prints the tiers; dry-running both rows costs nothing |
--aspect-ratio | not passed | the attached screenshots decide it — see "The frames decide the shape" |
--generate-audio | false | a docs clip has nothing to sync to, and the server's default is true. Omit the flag only when the clip genuinely wants a track |
--seed | let the script roll one, and keep it | it prints SEED and writes it to the clip's .json sidecar, which is what lets "that take, at a higher resolution" be re-submitted at all. It does not reproduce it: measured, an identical request on a fixed seed came back a visibly different clip. A byte-identical prompt is necessary and not sufficient — tell the user a re-render is another roll aimed at the same shot before they pay for it |
--real-person | leave unset | a photoreal face in a capture is refused at submission on bytedance/seedance-2.5; true is untested there |
The model stays never-ask — the agent doesn't raise it. But never-ask is not "never listen": if the user names a model, use it.
Model ids, prices, resolutions, durations and aspect ratios are not written down in this file on purpose. They are catalog facts, they change, and this repo has five recorded defects that trace to a hardcoded copy of somebody else's value table. Read them live instead, free and with no API key:
bash ../ofox-video-core/references/ofox-video.sh models # models, tiers, ranges
bash ../ofox-video-core/references/ofox-video.sh providers # the price matrix
Worth telling the user before they switch: the text-preserving cross-fade was
measured on bytedance/seedance-2.5 only. The catalog says the current models
do i2v, but i2v does not distinguish "one end locked" from "both", and it
says nothing at all about what happens to lettering — so another model is an
untested route for the one property this whole scenario depends on.
Never submit a paid job until the user has seen a cost table and said yes.
The rule, the table's required columns and where the numbers come from are
written down once for every Ofox skill in this repo:
../ofox-video-core/references/approval-gate.md.
Get the numbers from --dry-run, which validates everything and prints the
estimate without sending a request:
bash ../ofox-video-core/references/ofox-video.sh generate --dry-run \
--prompt "..." \
--frame-first-image /path/to/before.png \
--frame-last-image /path/to/after.png \
--duration 4 --resolution 480p --generate-audio false \
--out-dir /absolute/path/to/out
Relay the Estimated cost: line it prints — never a number of your own — then
wait for a yes, then re-run the identical command with --dry-run removed.
The estimate a real run prints comes microseconds before the request goes
out, too late to relay.
The brief recap goes in the same message as the prompt and the table, above them.
The cost anchor this skill has, with the parameters it was measured at
attached, because a figure without them is not a measurement:
bytedance/seedance-2.5, 4 seconds, 480p, first+last frames, 44 cents
billed — twice, on jobs 5e59baa2 and 427278b4, at the same figure. It is
not a quote for any other combination — an image-to-video job bills at the
text-to-video tier, so a longer or higher clip scales from the dry run, never
from this number.
Afterwards the actual bill is VIDEO_COST from the finished job. Report
it as money, not as the raw ten-decimal string.
Worth saying out loud when the user is deciding: a screen recording of the real app costs nothing and is exact. This skill earns its keep when there is no running app to record — a design that isn't built, a state that is hard to reach, a before/after across two releases — or when a clean animated transition is wanted from captures that already exist.
The dry run's own output names the model, the duration and the resolution and
says nothing about the frames — so a mistyped flag, or a path that quietly
matched only one file, reads as a perfectly healthy quote and shows up in the
delivered clip instead. --print-payload closes that: it dumps the request
body on stderr before the dry run stops (the API key travels in a header,
never in the body), and the two frame_type entries are the part to read:
bash ../ofox-video-core/references/ofox-video.sh generate --dry-run --print-payload \
--prompt "..." \
--frame-first-image /path/to/before.png \
--frame-last-image /path/to/after.png \
--duration 4 --resolution 480p --generate-audio false \
--out-dir /absolute/path/to/out \
2>&1 >/dev/null | grep -o '"frame_type":"[a-z_]*"'
Measured output on a two-frame job:
"frame_type":"first_frame"
"frame_type":"last_frame"
Two lines, in that order, is the confirmation. One line means one of the two flags didn't take; a different order means the before/after captures are on the wrong flags, which is the backwards-clip failure below and is free to fix at this point. Grep the payload rather than printing it whole: each capture is base64-inlined into that JSON, so the body runs to megabytes.
--out-dir is worth passing to the dry run for the same reason — the script
creates and enters it here, and a path it can't use fails as exit 6 with
nothing submitted. Pass the same directory to the real run; a different
one afterwards re-opens the question the dry run just settled (an exit 6
after a create costs the job's price, and is recovered with
poll JOB_ID --out-dir <writable dir>, never with a second generate).
The middle is generated, so it is a roll — and legibility is the thing being
rolled for. When the user wants options, batch prices the whole set up
front, waits on the takes concurrently, and builds a contact sheet:
bash ../ofox-video-core/references/ofox-video.sh batch --dry-run \
--prompt "..." --takes 3 \
--frame-first-image /path/to/before.png --frame-last-image /path/to/after.png \
--duration 4 --resolution 480p --out-dir /absolute/path/to/out
Quote BATCH_COST_TOTAL, not BATCH_COST_PER_TAKE, and give the takes a row
each. If one take in three is usable, that clip cost the whole total. At
--dry-run the total is the Estimated cost: ~$… for N takes line; the two
BATCH_COST_* keys are what the real run prints afterwards.
The contact sheet is three frames per take at thumbnail size, so it is good
for "did the layout hold" and not good enough for "is the text correct" —
open the individual takes for that. Hand the user the CONTACT_SHEET path on
its own line, then the take paths beneath it. Each TAKE line carries
seed=N, which is how a take is named and re-submitted higher — an aim at
that take, not a promise of it (see the --seed row above).
Submission is sequential and only the waiting runs in parallel, so the whole total is committed within seconds of the yes — the cost table is the only place a batch can be stopped.
models, providers and generate --dry-run all work with OFOX_API_KEY
unset. So when a user hasn't signed up yet, quote the job first and let them
decide whether it's worth registering — don't open by sending them to a
signup form.
bash: ../ofox-video-core/references/ofox-video.sh: No such file or directory
Nothing is broken — this skill delegates all execution to ofox-video-core
and reaches it by relative path, and that path just missed. Two different
situations wear this message, so run the probe in "Where the core skill lives"
before deciding which:
ofoxai-skills-ofox-video-core). Re-run the command against what the probe
printed. Nothing needs installing.ofox-video-core really is absent, and the
fix belongs to whichever installer the user already has: npx ofox-skills
(this repo's own) or the underlying
npx skills add ofoxai/skills --skill '*' --agent '*' --global --yes for
skills.sh; on LobeHub or ClawHub, install ofox-video-core from the same
publisher.Either way, say which skill is missing and where it was expected rather than relaying the raw path error, which names neither.
A broken link to a shared reference has the same two causes: this skill
packages only its SKILL.md and CHANGELOG.md, so prompt-structure.md,
creative-brief.md, approval-gate.md and api-params.md are out of reach
whenever the script is. The skill still works without them — the template, the
question set and the defaults are written out here.
Full table in ../ofox-video-core/SKILL.md.
The ones that come up:
| Code | Meaning | What to do |
|---|---|---|
1 | Parameter rejected locally, no network call, nothing billed | Fix the flag and retry freely |
2 | Environment problem — curl/jq missing, or no OFOX_API_KEY | Ask the user to fix it; check reports the same |
3 | API rejected it, or the job ended failed/cancelled/expired | Read the mapped message; a rejected create was not billed |
4 | Timed out waiting — the job is still running and billable | poll JOB_ID, never re-run generate |
5 | Ambiguous network failure on create | Do not retry blindly; check https://app.ofox.ai first |
6 | --out-dir unusable | Fix the path; if it happened after a create, poll JOB_ID |
generate blocks while it polls, up to --max-wait (default 540s). A short
low-resolution clip is usually one to three minutes. Say so before starting,
so the wait isn't silent.
If your tool call can't stay open that long, use create (submits and returns
a job id in seconds) followed by poll, instead of generate. That way a
timeout can never strand a job whose id you never saw.
Always pass --out-dir, and make it an absolute path. Without it the
script writes to the current working directory — which, given that the
examples here run from this skill's own directory, would drop the user's video
inside an installed skill. Relay the absolute VIDEO_PATH the script
prints, on its own line.
The dry run is where a bad directory gets caught: generate --dry-run (and
batch --dry-run) creates and enters --out-dir before it quotes, and exits
6 without submitting anything if it can't. That only covers the directory
you actually passed it, so give the real run the same one — swapping it
afterwards puts you back to discovering the problem after the job is paid for.
Pass --name too — name the file after the state change rather than leaving
the script to guess from the prompt's opening words, which here describe a
window size. The clip lands as <name>-<short job id>.mp4 with a .json
sidecar holding the full job id, the prompt, the seed and the real cost.
bash ../ofox-video-core/references/ofox-video.sh generate \
--prompt "<the UI-transition prompt built above>" \
--name "<short description, e.g. plan upgrade starter to business>" \
--frame-first-image "/absolute/path/to/before.png" \
--frame-last-image "/absolute/path/to/after.png" \
--duration 4 \
--resolution 480p \
--generate-audio false \
--out-dir /absolute/path/to/out
No --aspect-ratio on purpose, on any model — see "The frames decide the
shape".
This one call validates the parameters, submits the job, polls to completion,
downloads the mp4, and prints STATUS, JOB_ID, VIDEO_PATH,
VIDEO_SECONDS, SEED and VIDEO_COST. Report the actual values from
that output — never the estimate, and never a path or cost you didn't see the
script print. Do not re-implement any of the request, poll or download logic
here.
| Symptom | Cause | Fix |
|---|---|---|
Exit 1, no network call made | A flag the model doesn't accept — a duration outside its range, a resolution it doesn't have | Read the error, which names the model's real values, and re-run. Nothing was submitted, so retrying is free |
Exit 1, references_conflict | --frame-first-image/--frame-last-image together with an input_references array in --extra-json | Pick one meaning — locked frames, or identity references — and drop the other. They cannot be combined in one job |
Exit 3, input_moderation_failed | A capture contains a photoreal face. Refused at submission on bytedance/seedance-2.5; nothing billed | Crop or blur the face and retry. --real-person true is untested on that model here |
Exit 3, insufficient_credits | Ofox balance too low | No charge was made; add credits at https://app.ofox.ai and retry |
Exit 3, job ends failed with output_moderation_failed | The generated output failed a post-generation check — after the job ran, not at submission. Not billed | Retry as a brand-new generate with a different prompt or captures. A new request, not a resubmission |
| The clip runs backwards | The two paths were passed on the wrong flags | Swap --frame-first-image and --frame-last-image. New job, new cost table — which is why Order is a must-ask when the input is ambiguous |
| Text that should not have changed goes soft or doubles mid-clip | The measured runs kept unchanged labels solid on one pair at 480p, twice. Fine lettering, a denser layout or a lower tier are all outside that run | Re-render at the next resolution tier and compare the same mid-frame; if it persists, reduce how much differs between the two captures. New job, new cost table |
| A changed value is spelled wrong mid-fade | Outside what was measured — on job 5e59baa2 the mid-fade values were already correct, and on 427278b4 the final-frame values were | Check the prompt's CHANGE line quotes both values verbatim first; a typo there is the cheapest possible cause. Then re-roll, or cut the number of simultaneous changes |
| The layout drifts, reflows or scrolls | The two captures were not taken at the same window size, zoom or scroll position | Re-capture both without touching the window between them. The pair, not the prompt, is what holds the layout |
| A tooltip, toast, badge or cursor appears that is in neither capture | The model filled the middle with something plausible | Keep those items in the AVOID list (they are in the template), and re-roll. Elements that exist in neither endpoint are the one thing this mechanism is not doing for you |
| The output is the captures' shape, not the shape you wanted | Expected: with frames attached the clip follows the frames | Crop both captures to the target shape and generate again — a new job, billed again, which is why the crop happens before the first submission |
| Black bars are baked into the video | The captures were padded to a target ratio instead of cropped | Crop, re-run. Padding makes the bars part of the picture the model was handed |
Exit 4, timed out waiting | The job is still running upstream, not failed | Do not re-run generate; run poll JOB_ID with the id printed before the timeout |
Exit 5, ambiguous network failure on create | No HTTP response at all — can't tell whether a job was created | Don't guess or retry; tell the user to check https://app.ofox.ai for a job that may already be running |
Exit 6, --out-dir unusable | Local filesystem problem, not an API problem | The job is unaffected — fix the path and poll JOB_ID --out-dir <writable dir> rather than regenerating |
keyframe-animation, the same mechanism with different prompt
advice and its own measured behaviour.