本文へ移動
cccskills
無料GitHub で公開

ofox-image-core

Requires OFOX_API_KEY — create one at https://app.ofox.ai. Shared execution layer for the Ofox image API (api.ofox.ai) — validates parameters client-side, sends one synchronous request, base64-decodes the result, saves it to a file, and reports the real usage token counts and the computed dollar cost. Does both text-to-image (generate) and editing an existing image you supply as a local file (edit — change the background, recolour an element, alter a product photo, while leaving the rest of the picture intact). This is a library skill, not a standalone user-facing one — it is meant to be invoked by scenario skills that build model/prompt/size choices for a specific use case and then call into this skill's script rather than re-implementing the API calls — image-edit owns "change this image so that...", product-image owns a set of product images to choose between, and seedance-anime-drama owns character images for a video pipeline. Load this skill directly only when a user explicitly names the Ofox image API, asks to call it with specific low-level parameters, asks to debug a failed Ofox image request, or wants a plain "generate an image of..." from text — which is the one common case no scenario skill covers.

インストール方法を見る

含まれるファイル(14)

  • SKILL.md51.1 KB
  • CHANGELOG.md75.6 KB
  • references/api-params.md45.8 KB
  • references/models-snapshot.json6.9 KB
  • references/ofox-image.sh149.9 KB
  • references/pricing.md26.8 KB
  • references/refresh-snapshot.sh2.8 KB
  • references/test/dryrun.test.sh23.8 KB
  • references/test/edit.test.sh28.4 KB
  • references/test/imagecost.test.sh11.2 KB
  • references/test/keyguard.test.sh21.4 KB
  • references/test/targetsize.test.sh16.6 KB
  • references/test/validation.test.sh11.5 KB
  • references/token-anchors.json25.1 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

ofox-image-core: Ofox image API execution layer

Wraps the Ofox image generation API (https://api.ofox.ai/v1/images/generations) behind one script: validate, request, decode, save, report real token usage. Unlike ofox-video-core, this API is synchronous — one request either returns the finished image(s) in the response body or fails outright. There is no job id, no polling, and (because of that) no free "check what happened" recovery path if a request goes wrong mid-flight.

Safety contract (non-negotiable)

  • The script never reads a dotenv file. references/ofox-image.sh resolves OFOX_API_KEY from the shell environment only, and the key is never hardcoded in a script or a skill file.
  • You may load the key from a dotenv file once the user has authorized it. Locate it (.env at the repo root is the usual spot), then set -a; . <path>; set +a in the shell you'll call the script from. Sourcing a dotenv pulls in every variable in the file, not just the key — OFOX_API_BASE_URL is one this script reads, and it redirects every API call — so read the file before you load it. Never echo the value.
  • The key goes to exactly one host, and you are told when that host is not Ofox. Since 1.14.0 OFOX_API_BASE_URL must be https:// (a loopback host may be http://, for a local test server) and an override prints a NOTE: on stderr naming the host that will receive the key. Relay that line if you see it. Every request this script makes is built from that base — no URL out of a response body is ever fetched — so there is nothing else to check.
  • edit --extra-form will not read local files. A value starting @ or < is refused (curl reads those as "upload this file" / "send this file's contents"). Use --image to send an image; --extra-form is for extra fields.
  • Neither escape hatch can set a field that has a flag. --extra-json (generate) and --extra-form (edit) are both refused for model, prompt, quality, size, n, output_format, background — and --extra-form for image/image_url too. Those are validated per model and priced before the hatch is applied, so a value arriving that way would change what is billed after the quote the user approved: --extra-form "n=10" printed N 1 and asked for ten images. Pass them as flags.
  • Never print, log, or echo the raw key value — not in chat, not in a file, not in a command you show the user, not in verbose curl output. references/ofox-image.sh never uses curl -v/--trace for exactly this reason (those would print the Authorization header). If you need to show that a key is configured, say "OFOX_API_KEY is set" — never the value.
  • Never write the key into any file this skill (or a skill built on it) creates, including generated images' metadata, cost reports, or committed code.
  • Check once, then proceed. Run the availability/key check at most once per session (see below). If it passes, don't re-prompt for the key on every subsequent call in that session.
  • Fail open on a missing key — guide the user to get one, don't dead-end the conversation. A missing key means "can't call the paid API yet," not "stop talking to me."

Which model: the priority chain

bash references/ofox-image.sh models lists every image model Ofox serves, its per-output-token price, and the priority chain below together with the model it resolves to right now. No API key needed — GET /v1/models is public.

--model is optional. Omit it (or pass auto) and the script walks a cost-per-image-ranked chain and uses the first model that is actually available:

ModelCost per image (measured — always at the pair named)Why it is here
1openai/gpt-image-2~0.6 cents at --quality low --size 1024x1024; ~15.4 cents at --quality high --size 1792x1024cheapest per image at the one pair measured on both it and row 2, despite the higher per-token rate below — see the warnings
2microsoft/mai-image-2.5-flash~2.0 cents at --quality low --size 1024x1024second cheapest at that pair; was the chain's preferred model through 1.3.0
3google/gemini-3.1-flash-lite-imagenot measuredtied with (1) on the per-token rate, but no per-image figure recorded yet
4microsoft/mai-image-2.5not measuredsame vendor as (2), when quality matters more

⚠️ There is no such thing as "the" per-image price of an image model — every figure above is only true for the --quality/--size pair beside it. Measured 2026-09-04: openai/gpt-image-2 spends 196 output tokens at low / 1024x1024 and 5063 at high / 1792x1024, so the same model costs 0.6 cents or 15.4 cents depending on two flags — a 26x spread. Quoting a bare "~0.6 cents per image" for a large, high-quality frame is exactly the mistake that produced a 15.4-cent bill against a 0.6-cent estimate. Since 1.7.0 --dry-run picks the measured point matching the request's own pair, and falls back to the dearest one labelled UPPER BOUND when the pair has never been measured — so the estimate no longer under-quotes, but a bare per-image figure quoted from this table by hand still can. See "The estimate is rough, and sometimes impossible" below.

⚠️ This table is ranked by measured cost per image, not by the rate card's per-output-token price — the two rankings disagree here. At the one pair measured on both models (low / 1024x1024, 2026-09-02), mai-image-2.5-flash spends 1024 output tokens against gpt-image-2's 196, so the more-expensive-per-token model is ~3.4x cheaper per image (0.6 cents vs 2.0 cents), and that measurement is why the chain was reordered — see "The 2026-09-04 reversal" below. The token counts are the measurement and have not moved; the cents and the ratio are the vendor's rate card multiplied through, and that moved on 2026-09-15 (this row read 2.67 cents and 4.5x until Ofox cut mai-image-2.5-flash's per-token rate by 25%). Two further limits on how far the ranking reaches: rows 3–4 have no per-image measurement at all, only the per-token rate; and no like-for-like comparison exists at any other pair — the 26x within-model spread above is larger than the ~3.4x between-model one, so "which model is cheaper" is a narrower claim than it looks. Do not re-derive the order from the rate card alone — the comparable figure is rate x that model's own measured token count at the pair you intend to run. Full numbers and the caveats on them: references/pricing.md.

The chain is defined in exactly one place — MODEL_CHAIN in references/ofox-image.sh — and every skill built on this one resolves a model by calling that script, never by keeping its own copy of the list. The cents above are the catalog's rates as of 2026-09-15 applied to token counts measured on 2026-09-02, quoted here to explain the ordering; the script reads live rates, so a repricing moves the estimate without invalidating this table's argument. It moved once already — see the note in the warning above.

It is a priority, not a lock. --model <id> still pins any image model Ofox serves, including far more expensive ones. The chain only decides what happens when nobody picked.

The 2026-09-04 reversal

MODEL_CHAIN's order has changed exactly once, and the change is recorded here so it never reads as someone quietly sneaking a preference through.

On 2026-09-02, with both mai-image-2.5-flash's and gpt-image-2's per-image costs already measured (the table above), the repo owner looked at the two figures and deliberately kept mai-image-2.5-flash first — recorded at the time in references/token-anchors.json's chain-order history note, together with the instruction that whoever next wanted to reorder the chain should raise it first rather than doing it quietly. Cost was one input to that choice, not the only one.

On 2026-09-04 the repo owner raised it, and asked for openai/gpt-image-2 first instead. MODEL_CHAIN and the table above now reflect that request. This is the reversal it asked for, not an unreviewed reordering — and the 2026-09-02 rule is still in force for the next person who wants to change this again: raise it before you reorder it.

gpt-image-2 has exactly one real run behind it in this repo, and it carried two surprises. Until 2026-09-04 it had none at all — its ~0.6-cent figure was a token count (2026-09-02, three calls) rather than a look at what the model draws, while every opening-frame / concept image this skill had actually produced came from mai-image-2.5-flash. On 2026-09-04 a scenario skill generated a real frame with the new default, at --quality high --size 1792x1024, and it:

  • cost 15.4 cents, not 0.6 — 5063 output tokens, ~26x the anchored estimate, because the anchor was measured at low / 1024x1024 and nothing recorded that (the chain table's first warning above);
  • was refused once first, because this model does not accept --quality standard — the value every earlier image in this repo used (see "Required flags" below). Nothing was billed for the refusal.

It also honoured --size exactly, which neither model before it did (the SIZE gotcha below). One run is one run: pin --model microsoft/mai-image-2.5-flash explicitly if the look turns out to matter more than the price for a given use case, and look at the result rather than assuming it matches what mai-image-2.5-flash would have produced.

Both surprises are guards now rather than warnings — 1.7.0 made the estimate pair-aware and --quality per-model, so that run's quote would come out at the 15.4-cent point and its --quality standard would be refused at --dry-run for free. Neither guard makes the model's look predictable, which is the part still resting on a single run.

Fallback is reported, never silent. If the preferred model is missing from the model list, doesn't serve /v1/images/generations, or is deprecated, the script skips it and prints MODEL_FALLBACK_FROM, MODEL_FALLBACK_REASON and MODEL_PRICE_DELTA (e.g. "1.80x the preferred rate"). Relay all three — falling back to something more expensive is a decision the user may want to make differently.

The model is resolved before anything is quoted, so the id in a --dry-run quote is the id that would really run. There is deliberately no path where the model gets chosen after the user approves a price.

Why there is a default now, when there deliberately wasn't one before

This skill used to require --model and refuse to default it, reasoning that the models differ roughly 4x in price with no obvious winner, so defaulting would silently pick a price on the user's behalf. That reasoning was right about the risk, and the risk is now paid for rather than avoided: --dry-run plus the approval gate put the resolved model and its cost in front of the user before every spend, default or not. The default no longer decides anything quietly, which was the only thing wrong with having one. Weaken the gate and the default should go back.

Three models are documented in depth in references/api-params.md and references/pricing.md — openai/gpt-image-2, google/gemini-3.1-flash-image, qwen/qwen-image-3.0-pro. The rest work too; what isn't documented is which --size/--quality values each accepts, because the API doesn't publish that for image models. That gap covers the chain's models too: a --quality value a given model won't take surfaces as exit 3 with the upstream message, not as a client-side rejection — confirmed on 2026-09-04 by gpt-image-2 refusing --quality standard, and recorded per model under "Required flags" below.

Before you spend: quote it, get a yes, then spend it

Never send a generation request until the user has seen a cost table and said yes. The full spec — required columns, where the numbers must come from, how to itemise a batch, what to do when no estimate is possible — is shared by every Ofox skill in this repo and lives in one place: ofox-video-core/references/approval-gate.md.

That link, and the other two to the same file in this skill — one under "Why there is a default now" above, one in the scenario-skill notes below — assume the sibling directory is named ofox-video-core — true for skills.sh, ClawHub and npx ofox-skills, and not for LobeHub, which unpacks each skill as ofoxai-skills-<name>. If it doesn't resolve, probe before concluding the file is gone:

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/approval-gate.md" ] && echo "$d" && break
done

Nothing found means ofox-video-core isn't installed at all. The gate still applies — its rule is the paragraph above and the dry run below. The spec's detail comes back with that skill, and installing it is the user's call, not yours: hand over the command for whichever installer they already have rather than running it. skills.sh is npx skills add ofoxai/skills --skill ofox-video-core, this repo's wrapper is npx ofox-skills ofox-video-core, and on LobeHub or ClawHub it is ofox-video-core from the same publisher. Ask for the one missing skill rather than the whole repo, and give all three routes — naming one installer tells a user of the other two to abandon theirs.

What this skill contributes to that table:

bash references/ofox-image.sh generate --dry-run \
  --prompt "..." --quality high --target-aspect 16:9 --out-dir ./assets

(high, not standard: the chain's default model does not accept standard, and since 1.7.0 this dry run refuses the combination itself rather than letting it reach an HTTP 400 — see "Required flags" above. And --target-aspect 16:9 because this frame is going into a video job — the size enum cannot express 16:9, so the crop is not optional; the dry run reports which --size will be requested to serve it, and prices the estimate at that size rather than at whatever pair the model's anchor happened to be measured at.)

--dry-run parses the arguments, resolves the model against the chain, validates every parameter, creates and checks --out-dir, builds the payload and prints the estimate — then returns, before the POST. Nothing is submitted, nothing is billed, and no OFOX_API_KEY is needed, so a job can be priced before anyone signs up. It prints STATUS dry_run, MODEL, QUALITY, SIZE/N where they apply, TARGET_ASPECT/TARGET_SIZE when a target was set, the MODEL_FALLBACK_* lines when a fallback happened, and MODEL_CHAIN_EXHAUSTED in the rarer case where nothing in the chain is usable — the model named is then one the script already expects the API to reject, which the user needs to hear before approving anything.

The estimate is rough, and sometimes impossible — say which

Video can be quoted exactly: duration is an input and the rate is per second. Images cannot. They bill per output token, and the token count only exists in the response. So --dry-run prints exactly one Estimated cost: line, and it is one of two things:

  • ROUGH ~<amount> (<N> output tokens x ..., measured <date> at --quality <q> --size <s>) — anchored to a real measured call with that same model at that same pair. Relay it as rough; it is not a quote.
  • ROUGH UPPER BOUND ~<amount> (... measured <date> at --quality <q> --size <s>) — nothing has been measured at the pair this request asks for, so the figure is the dearest point that model has, quoted as a ceiling. Relay it as a ceiling and say so: the real bill may land well under it.
  • cannot be predicted for '<model>' — no real call's output-token count has been recorded for it — no measurement exists yet. Relay that sentence, show the table anyway with "cannot be predicted" in the cost column, and still wait for a yes.

An anchor is only valid for the --quality/--size pair it was measured at, and since 1.7.0 the lookup knows that. It used to read one number per model and print it for every request, which cost real money on 2026-09-04: a run at high / 1792x1024 was quoted from gpt-image-2's low / 1024x1024 anchor — approved at ~0.6 cents, billed 15.4 cents, 26x, with the size and the quality on screen directly above the wrong price. The rule now:

  1. a measurement at the request's own pair is quoted as-is;
  2. otherwise the dearest measurement that model has is quoted, labelled UPPER BOUND and carrying the pair it came from, so a table built from the line alone still shows whether the number belongs to what was asked for;
  3. nothing is interpolated between two measured points, and no quote comes out below what the request could cost;
  4. auto for either flag — and an omitted --size, which is the same thing, the API picking — is unknowable in advance and takes the upper-bound path.

Two consequences worth carrying into the table. A common cheap call now over-quotes: --quality low with no --size on gpt-image-2 shows the 15.4-cent ceiling, not the 0.6-cent point, because an omitted size cannot be matched — name the pair when you want the exact figure. And the ceiling bounds the output-token half only; the prompt's input tokens are excluded from every figure this script estimates, as they always were, which on observed calls was a fraction of a cent.

Never borrow one model's measured token count for another model. A borrowed number is indistinguishable from a measured one and is worth less than no number at all. Measured anchors today: google/gemini-3.1-flash-image (invoice-checked), microsoft/mai-image-2.5-flash and openai/gpt-image-2 (formula only) — each at the pair recorded beside it, and the chain's rows 3–4 at none. See references/pricing.md.

Availability check

Before the first call in a session, verify the environment:

bash references/ofox-image.sh check

This checks curl, jq, and OFOX_API_KEY and exits 0 only if all three are present — it makes no network call. Handle each failure mode plainly:

  • curl missing (rare — usually preinstalled): brew install curl (macOS) or sudo apt-get install curl (Debian/Ubuntu), else https://curl.se/download.html.
  • jq missing: brew install jq (macOS) or sudo apt-get install jq (Debian/Ubuntu), else https://jqlang.org/download/.
  • OFOX_API_KEY missing: two paths, and the second one is the one agents forget. If the user has no key, they get one at https://app.ofox.ai (log in → Settings → API Keys → Create New Key, shown once) and export OFOX_API_KEY=... in their shell. If they say the key already lives in a file, don't send them back to the terminal to re-type it — load it yourself with set -a; . <path>; set +a in the shell you'll call the script from (see the safety contract: authorization first, read the file before sourcing it, never echo the value). Offer this once, plainly, and move on — don't repeat the pitch on every message.

check deliberately does not test for ffmpeg/ffprobe: they are only needed by --target-aspect/--target-size, and making them a session-level requirement would block callers who never crop. Those flags check for them themselves, before anything is spent (exit 2).

Invoking the script

bash references/ofox-image.sh generate \
  --prompt "..." --quality VAL [--model MODEL] [OPTIONS]

There is a second subcommand, edit, for changing an image you already have rather than drawing a new one — see "Editing an image you already have" below. Everything in this section describes generate; edit shares the model resolution, the --dry-run contract, the exit codes and the geometry flags, and differs in the three ways that section lists.

generate resolves the model, validates every parameter client-side (model name, size, quality, and the documented n + Gemini incompatibility) before any network call, builds the request, sends exactly one POST, and on success base64-decodes each returned image and writes it to a file. It prints:

STATUS completed
IMAGE_PATH <absolute/path/to/file.ext>   (one line per generated image)
MODEL <the model the cost below is priced at>
MODEL_SOURCE response | request
MODEL_REQUESTED <id>                     (only when it differs from MODEL)
SIZE <size the response claims — not evidence of anything, see below>
SIZE_ACTUAL <WxH measured from the written file, or "unmeasured">
SIZE_FINAL <WxH of the cropped deliverable>   (only with --target-aspect/--target-size)
TARGET_ASPECT <W:H>                           (only with --target-aspect/--target-size)
IMAGE_PATH_UNCROPPED <path>                   (only when a crop happened)
QUALITY <quality actually used, from the response>
USAGE_INPUT_TOKENS <n>
USAGE_OUTPUT_TOKENS <n>
USAGE_TOTAL_TOKENS <n>
IMAGE_COST <dollars>

MODEL_SOURCE says where the MODEL id came from, and it matters for two different reasons:

  • response — the API echoed it. Normal.
  • request — the response carried no model field, so the id is the one that was requested, not one the API confirmed. The cost is still computed, at that model's published rates. Relay it as "the API didn't echo a model name; priced as the model we asked for" rather than presenting it as confirmed.

⚠️ Which of the two you get from openai/gpt-image-2 has changed under us, so read the line rather than predicting it. On 2026-09-04 that model omitted the model field on every call observed — first-hand, and costly enough to be certain about: it defaulted to the literal "unknown", found no rate for it, and printed no IMAGE_COST at all on two paid calls, which is what 1.3.0's fallback was built to fix. This file then generalised that into "MODEL_SOURCE request is now the common case for a default generate call".

That generalisation no longer holds. Re-checked 2026-09-15 on three real paid calls against the same model — two probes run while image-edit and product-image were written (one generate, one edit) and one further minimal generate --quality low --size 1024x1024 (196 output tokens, 0.59 cents, IMAGE_COST 0.00593 against a 0.59-cent estimate) — all three printed MODEL_SOURCE response. The field is being echoed now.

Three days of observation eleven days apart is not a schedule, and nothing here says which way it will read tomorrow. What it does establish is the rule worth keeping: an upstream response shape is not a stable fact to write a "common case" sentence about. Both branches are handled, both are correct, and the script prints the line unconditionally precisely so a caller never has to guess. Relay what it printed on this run; do not tell a user which one to expect.

MODEL_REQUESTED appears only when upstream ran a different model from the one asked for (routing, aliasing, a silent downgrade). When it appears, MODEL and IMAGE_COST describe the model that actually ran — that is what the invoice will say — and anything you measure from that run belongs to MODEL, not to MODEL_REQUESTED. Surface the mismatch to the user; it is the difference between a bill that reconciles and one that does not.

Required flags: --prompt, and --quality (one of auto low medium high standard hd — Ofox's docs mark this required, and not every value applies to every model, so the script never guesses a default; you must pass one explicitly).

--quality is a per-model enum, and since 1.7.0 it is validated against the model the request will actually use — not just against the union of every model's values. So an invalid combination fails at --dry-run, exit 1, no network call, instead of costing a round trip to an HTTP 400. What is observed, and which half of it is first-hand:

ModelAccepted setWhere that comes from
openai/gpt-image-2 (the chain's head)auto, low, medium, high — enforced, so standard and hd are rejected locallyFirst-hand, the API's own enumeration: HTTP 400, Invalid value: 'standard'. Supported values are: 'low', 'medium', 'high', and 'auto' (2026-09-04, nothing billed)
microsoft/mai-image-2.5-flashthe full union — permissive fallbackstandard accepted on nine real runs. Evidence a value works, not an enumeration of what the model takes
google/gemini-3.1-flash-imagethe full union — permissive fallbacklow accepted on three real runs. Same limit
everything elsethe full union — permissive fallbackno observation either way

Only the first row is a whitelist, and that is deliberate. A narrower set invented for a model that never enumerated its own would reject calls that would have worked, and a false rejection is worse here than the 400 it prevents: the 400 costs a round trip and no money, while a false rejection blocks the work outright with no way around it short of editing the script. So an absence of evidence stays permissive, and a row is added only when a model has enumerated its own set. The table lives in model_qualities() in references/ofox-image.sh, with the refusal quoted next to it.

The trap this closes is worth stating plainly: standard used to work on every call this repo made, and stopped working the moment the chain's head became gpt-image-2 on 2026-09-04 — same flag, same script, instant 400, and --dry-run caught none of the five copy-pasteable commands that shipped broken in two other skills. It now does. Any caller still passing --quality standard needs high (or low/medium/auto), or --model microsoft/mai-image-2.5-flash pinned to keep the value; the error names the resolved model and both fixes, including when no --model was ever passed and the id came from MODEL_CHAIN. hd has never been accepted by any model here. Per-model detail and the captured response: references/api-params.md.

Optional flags: --model (any image model Ofox serves; default: the priority chain above), --dry-run (validate, resolve, quote, stop — no request, no key needed), --size (one of the documented WxH values or auto), --target-aspect W:H / --target-size WxH (the ratio or exact pixels the delivered file must really be — see "The size enum cannot express 16:9 or 9:16" below; this is the flag to use for a video first frame), --n (1-10, default 1 — not supported at all by google/gemini-3.1-flash-image, rejected client-side before any network call if combined with that model, even --n 1), --output-format (png/jpeg/webp), --background (transparent/opaque/auto), --extra-json '<json>' (a JSON object of fields not covered by a flag, e.g. extra_body.provider.type for openai/gpt-image-2 — it may not be empty and may not set a field that has a flag; see below), --out-dir (default: current directory), --out-name (bare filename, no extension, no path separators — default: a timestamp-based name). Full parameter reference: references/api-params.md. The cost formula and the invoice it was verified against: references/pricing.md.

Always state the full IMAGE_PATH as its own standalone line in your reply to the user — not folded into a sentence or buried mid-paragraph, mirroring ofox-video-core's VIDEO_PATH discipline exactly. --out-dir is resolved to an absolute path (and created if missing) before any network call, so relay the printed path exactly; the file's location is the actual deliverable, and the user should be able to find it without re-deriving your working directory.

The size enum cannot express 16:9 or 9:16

This is the single fact that explains every size surprise in this skill, and it is not a model bug — it is a structural gap between this image API's size enum and the video API's aspect ratios. Every value --size accepts, with its true ratio:

--sizeRatioWhat it actually is
1024x1024, 512x512, 256x2561.00001:1, exact
1536x10241.50003:2, exact
1024x15360.66672:3, exact
1792x10241.7500the closest thing to 16:9 — but 16:9 is 1.7778
1024x17920.5714the closest thing to 9:16 — but 9:16 is 0.5625

16:9 and 9:16 cannot be requested at all, on any model, at any quality. So every 16:9 or 9:16 frame this API produces has to be cropped. There is no flag, no model and no prompt wording that avoids it, and looking for one is the wrong search.

Why that matters more than tidiness: adaptive propagates the error

These frames mostly exist to be attached to a video job, and attaching an image to bytedance/seedance-2.5 forces aspect_ratio: adaptive — the frame's own ratio becomes the finished video's ratio, whatever the video flags say. A 1.75 frame yields a 1.75 video. Fixing it after the fact means paying for the video again, at video prices, so a ratio error in a two-cent image is charged at the cost of the clip it was attached to.

--target-aspect / --target-size: state it, and the script keeps it

# A 16:9 first frame for a video job — the common case.
bash references/ofox-image.sh generate \
  --prompt "a teenage girl at the edge of a school rooftop at sunset, ..." \
  --quality high --target-aspect 16:9 --out-dir ./assets

What that does, in order:

  1. Picks the request size from the enum above — the one that survives the crop with the most pixels intact (1792x1024 for 16:9, keeping 98.4%, against 1536x1024's 84.4%). An explicit --size is respected and never silently replaced; it is only warned about when it cannot cover the target.
  2. Generates as normal, at the usual price for that size and quality.
  3. Measures the written file with ffprobe — never the response's own size field, for the reasons in the next section.
  4. Centre-crops to exactly the target ratio. Crop dimensions are integer multiples of the reduced ratio, not a rounded division, so the result is 16:9 rather than 1.7773. Both crops on record fall out of that one rule: 1344x768 → 1344x756 and 1792x1024 → 1792x1008.
  5. Fails loudly if the target cannot be met — a file smaller than the target, or a --target-size no accepted size can cover without upscaling. It crops and scales down, never up. An almost-right image is the expensive outcome here, not the safe one.

--target-size WxH is the same thing with exact pixels: it requests the cheapest size that clears the floor after cropping (since anything above it is tokens bought and thrown away), then scales the crop to exactly WxH. The two flags are mutually exclusive — --target-size already fixes the ratio.

Which file to attach. The cropped frame takes the plain output name, so IMAGE_PATH is always the file that meets the target and an existing caller parsing that key gets the right one without changing. The API's untouched bytes are kept alongside it as <name>-uncropped.<ext> and reported as IMAGE_PATH_UNCROPPED, so a human who wants a different crop still has the original. If the crop fails, the plain name is never written and the run exits non-zero — there is no state in which a wrong-ratio frame is sitting where the right one was promised.

These flags need ffmpeg/ffprobe (brew install ffmpeg, sudo apt-get install ffmpeg) — the same dependency ofox-video-core already uses for contact sheets and chain frames, not a new one. Missing, they fail as an environment error (exit 2) before anything is spent, because the flags are a promise about the delivered file and there is no way to keep it without measuring and cropping. Everything else in this script, including --dry-run pricing, works without ffmpeg; drop the flag and you own the measure-and-crop step by hand.

Known gotcha: SIZE in the printed output can be wrong

Three models, three different behaviours, and the correction is needed in all three cases:

ModelRequestedResponse saidFile really wasRuns
google/gemini-3.1-flash-image512x512512x5121024x10241 (2026-08-29)
microsoft/mai-image-2.5-flash1792x10241354x7741344x7683
openai/gpt-image-21792x10241792x10241792x10241 (2026-09-04)

google/gemini-3.1-flash-image appears to always generate at its native 1024x1024 and echo back whatever size was asked for — verified with file and sips -g pixelWidth -g pixelHeight.

microsoft/mai-image-2.5-flash is worse than a two-way mismatch — it is a three-way one. Request, response echo and file on disk are three different numbers, on three separate paid runs. Evidence: the refs/ entries in content/ofox-cases/{anime-rooftop-confession, yunqi-sparkling-ad, sneaker-motion-ad}/case.json in the home-page project (a downstream consumer of this skill, not part of this repo).

openai/gpt-image-2 is the one model measured here that honours --size exactly — and it is one run, so read it as one observation, not a guarantee. qwen/qwen-image-3.0-pro is untested.

Honouring the request is not the same as producing a usable ratio, which is why that run still needed a crop: 1792x1024 is 1.75, so the frame went to 1792x1008 before it could be attached. So the rule is the same on every model and only the size of the correction changes — 16 pixels of height for gpt-image-2, a different resolution entirely for the other two.

SIZE is printed straight from the response and inherits all of this; SIZE_ACTUAL is measured from the file and is the one to believe. When they disagree the script says so on stderr rather than leaving it to be noticed. Don't trust SIZE, and crop rather than pad so nothing is invented at the edges — or pass --target-aspect/--target-size and let the script do both. Full detail: references/api-params.md.

Editing an image you already have (edit)

generate draws a new image from text. edit takes a file you already have and changes it — POST /v1/images/edits, added in 1.11.0.

bash references/ofox-image.sh edit \
  --image ./photo.png \
  --prompt "Replace the background with a beach at sunset. Keep the person unchanged." \
  --out-dir ./assets \
  --dry-run

Drop --dry-run to run it. Keep --out-dir — without it the edited file lands in whatever directory you happened to be in.

A local file is the primary input and needs no hosting. --image-url takes a public URL or a data: URI if you have one, but nothing requires you to put the picture on the internet first.

Output is the same shape as generate, plus the input echoed back for comparison and the token line that has no generate equivalent:

STATUS completed
IMAGE_PATH <absolute path to the edited file>
INPUT_IMAGE <absolute path to the source>
INPUT_SIZE_ACTUAL <WxH measured from the source>
MODEL / MODEL_SOURCE / SIZE / SIZE_ACTUAL / QUALITY   (as generate)
USAGE_INPUT_TOKENS <n>
USAGE_INPUT_IMAGE_TOKENS <n>    <- the uploaded picture, billed
USAGE_INPUT_TEXT_TOKENS <n>
USAGE_OUTPUT_TOKENS <n>
USAGE_TOTAL_TOKENS <n>
EDIT_COST <dollars>

Three things about edit that differ from generate

1. Not every model can edit, and the list is live. Run ofox-image.sh models --endpoint edits. Support is read from each model's supported_endpoints in the catalog, never from a table in this repo — confirmed both ways: a model without the flag is refused with endpoint_not_supported, models with it ran. Omitting --model resolves the priority chain against the edits endpoint, so a chain entry that could not edit would be skipped with a reason rather than silently failing.

2. The image you upload is billed. On the measured run, 576 of 608 input tokens were the picture. That component does not exist for a generation, and it scales with the file you pass: a 320x180 input cost 0.6 cents, an 854x480 input 1.4, and a 1792x1008 input 1.6 (1508 image tokens). Pass a smaller source when the job allows it — at the top end the uploaded picture is 74% of the bill (1508*0.000008 = 0.012064 of 0.016249), so it is the only lever that matters there.

⚠️ Do not reason about an edit's cost from its output tokens. They do not track the input: 129 (320x180), 229 (256x256), 301 (854x480), 129 (1792x1008) — the largest input ties the smallest for the lowest count. The input image tokens are what move with the file (240 / 256 / 576 / 1508), and they are the bigger half of the bill on anything large. See references/pricing.md → "Measured edits".

3. ⚠️ The API does not reject bad parameters here, so the dry run matters more, not less. generate gets a bad --quality refused upstream for free. edit does not — measured 2026-09-15, an unknown --quality value was silently ignored and rendered six billable images. The client-side checks in this script are the only guard. Run --dry-run first and read the FORM_FIELDS line to confirm what will actually be sent.

Verify the artifact, not the exit code

STATUS completed cannot tell you whether the endpoint edited your image or quietly redrew the prompt from scratch. Open the result next to INPUT_IMAGE and check that what you did not ask to change is unchanged. The script prints both paths side by side so this is one command, and prints a reminder on every successful edit.

For the record, on the runs this skill is built from it genuinely edits: a UI screenshot asked for one button colour change came back with every string intact ("Billing Settings", "$29.00", "Seats included 3") and 58% of all changed pixels inside the button, which is 1.1% of the frame. That is evidence about two runs on one model, not a guarantee about yours — which is why the check is in the instructions rather than assumed.

What is not established

  • Whether --size is honoured. All four measured runs omitted it and got a size derived from the input's aspect ratio at a near-constant pixel budget — 854x480, 320x180 and 1792x1008 (all 16:9) returned 1672x941, and 256x256 returned 1254x1254, which is 0.05% the same pixel count. The 1792x1008 run is the one that shows this is a budget rather than an upsample floor: its input is larger than the output and the output did not grow. Note the useful side: 1.777 is the 16:9 the generate size enum cannot express at all.
  • Whether --prompt is required by the API. This script requires it.
  • Whether a mask works. See "Out of scope" below.
  • The other four models that advertise the endpoint but have not been run.

Each of those costs a billed edit to answer, which is why they are written down as open rather than guessed at.

Out of scope for this script

  • input_images / image-to-image (Qwen-only field on this same /v1/images/generations endpoint) — this script only does text-to-image. Passing input_images via --extra-json is rejected client-side with a clear message.

  • Masked / inpainting edits. Whether POST /v1/images/edits accepts a mask is not established. Finding out costs a billed edit per attempt, so it was left alone rather than guessed at. Nor can you try it through edit --extra-form "mask=@FILE" any more — 1.14.0 refuses any --extra-form value starting @ or <, because that spelling uploads whatever local file it names, which made this flag a file-exfiltration primitive. Stating the narrowing rather than leaving it to be discovered: attaching a mask now needs its own flag with its own path validation, and nobody has needed one enough to write it.

    (POST /v1/images/edits itself is no longer out of scope — it is the edit subcommand as of 1.11.0. It is also not "OpenAI models only", as this list used to claim without evidence: eleven models advertise it and seven have been confirmed running an edit, across three vendors.)

  • Streaming responses (stream: true) — this script only parses a plain JSON response body. Rejected client-side if set via --extra-json.

🚨 This script writes NO sidecar — a paid run's numbers exist only on your terminal

Save the full output of every paid generate or edit before you close the shell. Redirect it to a file, or copy the figures into references/token-anchors.json immediately. Nothing does it for you.

ofox-video-core writes a .json sidecar next to every delivered mp4, carrying the job id, seed, prompt, request and real cost. This script writes nothing of the kind — an image lands as a .png and that is all. Verified: the three edits behind this file's 1792x1008 anchor produced flask-white-bg.png, flask-set-warm-wood.png and flask-set-grey-sweep.png with no companion file, while every video job in the same session has its sidecar on disk.

This is not a tidiness complaint — it has already nearly cost a measurement. The figures token-anchors.json's _how_to_add_a_row demands — USAGE_OUTPUT_TOKENS, the input token split, EDIT_COST — are exactly the values this path never persists. The 1792x1008 anchor was written up as "output tokens unrecorded and unrecoverable" and a never-under-quote disclosure was published on that premise; the numbers had simply scrolled out of reach, and were recovered from a session transcript rather than from disk. On the video side the same mistake is impossible, because the sidecar is already written.

So, concretely, when you make a paid image call:

bash references/ofox-image.sh edit --image IN.png --prompt "..." \
  --out-dir /abs/out 2>&1 | tee /abs/out/edit-$(date +%Y%m%d-%H%M%S).log

and take the anchor figures from that log rather than from memory. A billed run whose token counts are gone is money spent for an artifact you cannot price the next one from.

⚠️ Adding a sidecar to this script is a separate change and is deliberately not made here. It would need to decide the filename, the schema, and what happens on the --n > 1 path where one call writes several files — so it is a design question, not a one-line fix. Recorded as a recommendation rather than smuggled into a documentation pass.

No job id, no polling — what that means for failure handling

There is nothing to resume here the way ofox-video-core poll JOB_ID resumes a video job. A single generate call either:

  • gets a real HTTP response (success or a rejected request) — the request's fate is decided, and it is then safe to fix parameters and try again if it was rejected, or
  • gets no HTTP response at all (curl exits nonzero with no status code) — genuinely ambiguous, and unlike the video API there is no job id to look up afterward to find out what happened. The script exits 5 in this case and tells you to check your usage/billing history at https://app.ofox.ai before deciding whether to retry. Don't guess, and don't auto-retry a --extra-json-free, unmodified request on exit 5.

Whether a rejected (non-2xx) request is billed at all has not been confirmed for this endpoint — this script assumes, but has not verified with a real call, that a request producing no image is not charged (the common pattern for generation APIs, and the same assumption ofox-video-core verified for its own create-rejection case). Treat this as an open question pending a real, observed error case, not as a settled fact — if you do observe billing behavior on a real error, that's worth recording back into this skill's documentation.

Error handling

This endpoint's error vocabulary has not been broadly explored, and this skill's own earlier documentation got the error response's shape wrong, not just an unconfirmed code: the original research inferred error.code: "provider_type_unavailable" purely from Ofox's doc prose describing a provider-mismatch scenario — it was never seen in a real response. A real rejected call (2026-08-29, an invalid extra_body.provider.type, exactly the scenario that prose was describing) showed the actual shape is {"error": {"message", "type", "code"}} — an OpenAI-SDK-style shape, different from the video API's {code, message} shape where code really was a semantic string. Here, error.code was literally the HTTP status as a number (400); the real classifier is error.type (invalid_request_error was the only value confirmed until 2026-09-06, when a safety-system refusal on openai/gpt-image-2 returned a second one, image_generation_user_error — see the error table in references/api-params.md for the one trigger that has been isolated, and for the two repair routes: rewrite the clause, or re-run on --model microsoft/mai-image-2.5-flash. provider_type_unavailable still does not appear anywhere in a real response). print_api_error in ofox-image.sh now surfaces error.type as the primary classifier and shows error.code for reference, but always prints the raw error.message regardless — that part already worked correctly and still does. Two message-only gotchas remain doc-prose-only, unconfirmed by a real call: Gemini + /v1/images/edits ("Image editing is not supported for model") and Gemini + n (this script prevents the latter client-side before any network call, so it should never actually be observed against the real API through ofox-image.sh). Don't treat an unrecognized error.type as this script's bug; it means the vocabulary genuinely hasn't been seen yet — update references/api-params.md and this script's print_api_error if/when a new one is confirmed by a real call.

ExitMeaning
0Success — image(s) decoded and saved, usage token counts and IMAGE_COST printed. Also the exit code of a --dry-run, which prints STATUS dry_run and spends nothing.
1Usage/parameter validation error — no network call was made. Fix the flag and retry generate freely. Includes a --quality the resolved model does not accept, which since 1.7.0 lands here instead of as an exit 3 HTTP 400.
2Environment error — curl/jq/OFOX_API_KEY missing, or ffmpeg/ffprobe missing while --target-aspect/--target-size was passed. Fix the environment, no request was attempted.
3The API rejected the request, the response body couldn't be parsed into a usable image, or a --target-aspect/--target-size target could not be met by the file that came back (the image was generated and billed and is on disk at the -uncropped path; nothing wrong-ratio was written to the plain path) (see references/api-params.md for the one confirmed error.type; everything else is surfaced via the raw upstream message).
4--out-dir could not be created or entered (bad path, permissions) — caught before any network call, so no money was spent finding this out. Fix --out-dir and retry.
5Ambiguous network failure — no HTTP response received at all. No job id exists to check afterward; check https://app.ofox.ai's usage/billing history before deciding whether to retry.

For scenario skills built on this

A scenario skill (for example, one that generates a character reference image before handing it to ofox-video-core for image-to-video) should call references/ofox-image.sh generate rather than duplicating any of the request-building, validation, or decoding logic above. It owns the scenario-specific prompt template and recommended size/quality defaults; this skill owns the mechanics of talking to the API correctly and safely.

Four things a scenario skill must not re-implement, because a second copy is a second thing to forget:

  • The model choice. Don't hardcode a model id and don't keep a copy of the priority chain. Omit --model and let the script resolve it, or pass --model through when the user asked for a specific one. A scenario skill that pinned its own model quietly kept paying an old price after the chain moved — that is the failure this rule exists for.
  • The approval wording. Link ofox-video-core/references/approval-gate.md instead of restating the rule in your own words. Four paraphrases of "show the price first" become four different rules. Carry the directory-name caveat with the link, though — a scenario skill on LobeHub finds that file under ofoxai-skills-ofox-video-core, so a "Where the core skill lives" probe near the top of the skill is what keeps the link from reading as a missing file.
  • The measure-and-crop step. Don't tell the caller to check the file's pixels and crop it by hand — pass --target-aspect (or --target-size) and let the script guarantee the ratio. Three agents in a row re-derived that arithmetic before those flags existed, and a frame that skips it becomes a wrong-ratio paid video, because attaching an image forces aspect_ratio: adaptive. A scenario skill producing a video first frame should treat one of the two target flags as mandatory, not optional.
  • The per-image price, and which --quality a model takes. Don't write either into your own copy. A quoted cents-per-image figure goes stale the moment the chain moves or the flags change — gpt-image-2 is 0.6 cents or 15.4 cents depending on two of them — so relay the Estimated cost: line a --dry-run printed, including its UPPER BOUND label and the pair it names, rather than a number of your own. Likewise, don't hardcode a --quality value on the strength of one model accepting it: five copy-pasteable commands across two skills said --quality standard and broke the day the chain's head changed. Since 1.7.0 a --dry-run catches that for free, which makes dry-running your own documented command the cheapest way not to ship it broken.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Publish a static site (a folder of HTML/CSS/JS/images/fonts) to Cloudflare and get back a live, shareable URL in seconds. Use when you have a finished static page or site and need to hand someone a link they can open on any device — reports, mockups, one-off landing pages, AI-generated HTML, "give me a link I can share". Runs one packaged command (references/deploy.mjs) built on the Wrangler CLI, which is what Cloudflare's own agent guidance tells agents to use. Deploys permanently to your Cloudflare account when credentials are present, or as a 60-minute claimable preview when they are not — and says plainly which one you got. Bakes an expiry countdown into previews (matched to the real 60-minute limit; --ttl to shorten it), and supports optional server-side six-digit access-code protection with -otp for permanent links only. Unprotected delivery may fall back to a file; protected delivery fails closed.

日本語の概要は準備中です。原文の説明を表示しています。

ofoxai/skills22026年9月22日 更新

explainer

無料

Requires OFOX_API_KEY — create one at https://app.ofox.ai. Turn an article, doc or release note into a short explainer clip — one person to camera, or a voiceover over illustrative footage. The user supplies the source text and the model generates the speech; audio cannot be uploaded, measured. A 30-second clip holds about eighty spoken words in eight sentences — measured, and well under a tenth of a 1,200-word post — so this skill does not summarise an article, it picks the single idea worth saying and helps choose which one. Use when a user asks to turn writing into a short spoken video, e.g. "make a 30-second explainer from this blog post", "explain this feature in a short video", "turn our changelog into a clip", "a quick video explaining what this paper found". Do not use for a scene between people (see seedance-short-drama), a brand or product ad (see seedance-ad-creative), a handheld creator clip (see ugc-ads), or when the user already has both a portrait and the finished words (see talking-head). Budget sentences as well as words — each sentence boundary costs about 0.7 seconds of silence, so a script with more sentences runs longer at the same word count.

日本語の概要は準備中です。原文の説明を表示しています。

ofoxai/skills22026年9月22日 更新

hal-image

無料

Handle images with ImageMagick (magick) — read metadata, resize, crop, rotate, convert format, combine images (side-by-side or grids with montage), overlay logos/watermarks, and losslessly compress with oxipng before sending. Use when the user sends an image to process, when you produce an image to deliver, or before attaching any image to a message or upload — compressing first keeps transfers fast and tokens low. Core discipline - always compress before delivering, never expose local file paths to the user, and fail open (if a tool is missing or a step errors, pass the original through unchanged; never block the task).

日本語の概要は準備中です。原文の説明を表示しています。

ofoxai/skills22026年9月22日 更新

hal-vault

無料

Securely store, search, and use secrets (API keys, tokens, passwords, SSH keys) with hal-vault, an SSH-key encrypted local secret store. Use when the user shares a credential that should be saved, asks what secrets are stored or where a key is, or when a command/workflow needs a secret injected. Core discipline - never print raw secret values into chat, logs, or files; reference secrets only by their masked form, and use --reveal only to feed another process, never into a command argument (where it lands in the machine's process table) or into text you write down.

日本語の概要は準備中です。原文の説明を表示しています。

ofoxai/skills22026年9月22日 更新

Requires OFOX_API_KEY — create one at https://app.ofox.ai. Change one thing in an image you already have and leave the rest of the picture alone — swap the background, recolour a part, remove or add an object, clean up a photo — from a local jpeg/png/webp file. Delegates to ofox-image-core's `edit` subcommand (POST /v1/images/edits, one synchronous request), prices the job with --dry-run before spending, and reports the real token cost including the uploaded picture, which is billed. Use when a user hands over an image and asks for a change to it, e.g. "change the background of this photo to a beach and keep the person unchanged", "make this button green", "remove the car in the background", or "put this product on a plain white background". Do not use to draw a new image from a text description with no input picture (that is ofox-image-core's `generate`), to produce a set of several images to choose between (see product-image), or to turn a photo into video (see seedance-product-video or seedance-ad-creative). Editing the content of an existing video — "change the background of my clip, keep the product" — has no route here and none in the video API either (measured; the mode field an edit would use is accepted and silently ignored, so nothing errors); this skill edits a single still, and routing such a request to a video skill generates brand-new footage instead of changing theirs.

日本語の概要は準備中です。原文の説明を表示しています。

ofoxai/skills22026年9月22日 更新

Requires OFOX_API_KEY — create one at https://app.ofox.ai, plus two images you already have, a start frame and an end frame. Animates the motion between them as one video — both frames go into a single Ofox video job, the clip opens on A, closes on B, and the model fills the middle. Use when a user has two stills and wants the in-between animated, e.g. "here is the before and the after, animate the transition", "make a video that starts on this image and ends on that one", "tween these two frames", or "move the object from where it sits in the first picture to where it sits in the second". Do not use when only one image exists (animating a single frame is seedance-ad-creative or seedance-product-video), or when the pair is two states of a user interface (see product-demo).

日本語の概要は準備中です。原文の説明を表示しています。

ofoxai/skills22026年9月22日 更新

ofoxai のスキルをすべて見る

このスキルの問題を報告する