@duyquangnvx/image-gen 0.0.0-stage → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +520 -0
- package/README.md +213 -2
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +4446 -0
- package/dist/codex-C-kLMs4Z.mjs +282 -0
- package/dist/codex.d.mts +60 -0
- package/dist/codex.mjs +3 -0
- package/dist/errors-D6qZIFTT.mjs +210 -0
- package/dist/failure-8WRJSaJj.mjs +382 -0
- package/dist/index.d.mts +222 -0
- package/dist/index.mjs +3 -0
- package/dist/node.d.mts +12 -0
- package/dist/node.mjs +20 -0
- package/dist/options-Cf-GPJ_h.d.mts +238 -0
- package/dist/src-B1rS2YsP.mjs +1325 -0
- package/package.json +70 -4
- package/skills/image-gen/LICENSE.txt +201 -0
- package/skills/image-gen/SKILL.md +99 -0
- package/skills/image-gen/references/alpha.md +41 -0
- package/skills/image-gen/references/art-entries.md +38 -0
- package/skills/image-gen/references/confinement.md +21 -0
- package/skills/image-gen/references/prompting.md +116 -0
- package/skills/image-gen/references/repaint-sheet.md +140 -0
- package/skills/image-gen/references/sample-prompts.md +282 -0
- package/skills/image-gen/references/setup.md +45 -0
package/README.md
CHANGED
|
@@ -1,3 +1,214 @@
|
|
|
1
|
-
#
|
|
1
|
+
# image-gen
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generate, edit and cut out images through the AI SDK v7 `generateImage` surface, as a TypeScript
|
|
4
|
+
library and as an `image-gen` command. You bring the model; image-gen refuses what a provider would
|
|
5
|
+
quietly drop and throws when an image comes back as less than was asked for
|
|
6
|
+
([ADR-0001](./docs/adr/0001-prescribe-provider-options-and-throw-rather-than-degrade.md)).
|
|
7
|
+
|
|
8
|
+
[`CONTEXT.md`](./CONTEXT.md) holds the vocabulary, [`docs/design/library.md`](./docs/design/library.md)
|
|
9
|
+
the shape, [`docs/adr/`](./docs/adr/) the decisions, [`docs/recipes/`](./docs/recipes/) conventions
|
|
10
|
+
left out of the library, and [`CHANGELOG.md`](./CHANGELOG.md) what each release changed.
|
|
11
|
+
`CONTEXT.md` and `docs/` live in the source repository, not in the npm package.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Node 22.12 or later.
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm i -g @duyquangnvx/image-gen && image-gen skill install # the CLI, and its skill for Claude Code
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For the library: `pnpm add @duyquangnvx/image-gen ai`.
|
|
22
|
+
|
|
23
|
+
Inside Claude Code, type the CLI line as
|
|
24
|
+
`! npm i -g @duyquangnvx/image-gen && image-gen skill install`: the sandbox denies writes to
|
|
25
|
+
`~/.claude/skills`, and `!` runs outside it.
|
|
26
|
+
|
|
27
|
+
If `image-gen doctor` reports the registry `behind` right after an install, npm answered from a
|
|
28
|
+
stale cache: install again with its cache in a new folder,
|
|
29
|
+
`! npm i -g @duyquangnvx/image-gen --cache "$(mktemp -d)" && image-gen skill install`.
|
|
30
|
+
|
|
31
|
+
Once installed, the CLI upgrades itself to the latest release with `! image-gen update`: it asks
|
|
32
|
+
npm's registry for the version tagged `latest`, installs it with the npm beside the Node that runs
|
|
33
|
+
it, each npm call with a fresh cache so a stale one cannot hide a release, and has the new CLI
|
|
34
|
+
reinstall its skill when the skill was installed. `update` is not tested on Windows,
|
|
35
|
+
where npm may fail to replace the package the running `update` holds open; npm then rolls back, and
|
|
36
|
+
the manual lines above still work.
|
|
37
|
+
|
|
38
|
+
To uninstall, run `npm rm -g @duyquangnvx/image-gen`, then delete the skill in
|
|
39
|
+
`~/.claude/skills/image-gen`, the Plan Login in `~/.codex-imagegen`, and the keys in
|
|
40
|
+
`~/.config/image-gen/.env`.
|
|
41
|
+
|
|
42
|
+
`@ai-sdk/openai` and `@ai-sdk/google` ship with it; add `@ai-sdk/fal` for that provider. From a
|
|
43
|
+
clone, `pnpm install && pnpm build` builds `dist/`, and `pnpm link --global` then puts it on `PATH`.
|
|
44
|
+
|
|
45
|
+
## Library
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { openai } from "@ai-sdk/openai"
|
|
49
|
+
import { edit, generate } from "@duyquangnvx/image-gen"
|
|
50
|
+
import { readImage, writeImage } from "@duyquangnvx/image-gen/node"
|
|
51
|
+
|
|
52
|
+
const model = openai.image("gpt-image-2.5-flare")
|
|
53
|
+
|
|
54
|
+
const { image } = await generate({ model, prompt: "a ceramic coffee mug on a linen cloth", aspectRatio: "3:2" })
|
|
55
|
+
await writeImage("mug.png", image.uint8Array) // refuses an existing file unless { force: true }
|
|
56
|
+
|
|
57
|
+
// An Edit changes one target and keeps what the instruction did not name.
|
|
58
|
+
// References are material to draw from, not things to modify.
|
|
59
|
+
await edit({
|
|
60
|
+
model,
|
|
61
|
+
target: await readImage("portrait.png"),
|
|
62
|
+
instruction: "put the hat from image 2 on the person",
|
|
63
|
+
references: [await readImage("hat.png")],
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`generate`, `edit` and `removeBackground` return the images, the `modelId` that answered, `sizes`
|
|
68
|
+
measured from the bytes, `usage` and `warnings`. Their `onResend` (the `CallHooks` type) is called
|
|
69
|
+
with the `attempt` each time the AI SDK sends a failed call again, so a caller can say it is waiting.
|
|
70
|
+
|
|
71
|
+
`edit` returns the model's pixels as they came back, and a model may repaint beyond what was asked,
|
|
72
|
+
even beyond a Mask. `confineTo(target, { mask })` or `confineTo(target, { region })` builds, before
|
|
73
|
+
sending, the function that keeps an edited image of the target's size identical to the target outside
|
|
74
|
+
that Mask, or outside that rectangle and a fading band an eighth of its shorter side wide around it;
|
|
75
|
+
a Region is never sent, so it holds on any model.
|
|
76
|
+
|
|
77
|
+
### Transparency
|
|
78
|
+
|
|
79
|
+
Three ways to get an alpha channel, which are three different things
|
|
80
|
+
([ADR-0002](./docs/adr/0002-chroma-cutout-and-background-removal-stay-separate.md)):
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { fal } from "@ai-sdk/fal"
|
|
84
|
+
import { google } from "@ai-sdk/google"
|
|
85
|
+
import { generateCutout, removeBackground } from "@duyquangnvx/image-gen"
|
|
86
|
+
|
|
87
|
+
// The model paints onto an empty background: gpt-image-1*, gpt-image-2.5 and the ChatGPT plan.
|
|
88
|
+
await generate({ model: openai.image("gpt-image-2.5-flare"), prompt: "…", transparent: true })
|
|
89
|
+
|
|
90
|
+
// Generate on a flat Chroma Backdrop and key it out locally, on any model. Pick a colour the subject cannot contain.
|
|
91
|
+
const cutout = await generateCutout({ model: google.image("gemini-3.1-flash-image"), prompt: "a green frog", backdrop: "#ff00ff" })
|
|
92
|
+
|
|
93
|
+
// Cut a subject out of a photograph you did not generate. Costs a model call.
|
|
94
|
+
await removeBackground({ model: fal.image("fal-ai/bria/background/remove"), image: await readImage("photo.jpg") })
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A cutout cannot tell you whether it worked. `inspectCutout` reports the alpha at the four corners
|
|
98
|
+
(all `0` when no backdrop survived) and the Subject Bounds; `overCheckerboard` puts the image on a
|
|
99
|
+
known ground to judge its edge by eye; `fitToFrame` trims it to its subject and scales that whole,
|
|
100
|
+
centred, into a Target Frame; `splitPieces` separates an image holding several parts, such as a
|
|
101
|
+
kit, into one cropped image per Piece, reporting the specks it drops. None of them calls a model.
|
|
102
|
+
|
|
103
|
+
To hold one character across poses, see the [consistent-character recipe](./docs/recipes/consistent-character.md)
|
|
104
|
+
and `variantPrompt`.
|
|
105
|
+
|
|
106
|
+
### On a ChatGPT plan
|
|
107
|
+
|
|
108
|
+
`@duyquangnvx/image-gen/codex` is a model that posts to the ChatGPT plan's Images route, the one
|
|
109
|
+
Codex CLI calls, so generation bills the plan instead of API credit. Personal use only
|
|
110
|
+
([ADR-0004](./docs/adr/0004-the-codex-model-calls-the-plan-images-route-directly.md)). It keeps its
|
|
111
|
+
own login in `~/.codex-imagegen`, apart from your Codex CLI's. Sign in once, through the Codex CLI:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
image-gen login
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { codex } from "@duyquangnvx/image-gen/codex"
|
|
119
|
+
|
|
120
|
+
await generate({ model: codex.image(), prompt: "a ceramic coffee mug", transparent: true })
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The route always paints `gpt-image-2` at about 1536×1024, so only the shape can be chosen, and it is
|
|
124
|
+
written into the prompt ([ADR-0005](./docs/adr/0005-the-codex-model-writes-the-requested-shape-into-the-prompt.md)).
|
|
125
|
+
It takes at most five images per call, as bytes, and no Mask.
|
|
126
|
+
|
|
127
|
+
### What it throws
|
|
128
|
+
|
|
129
|
+
Every error of its own is an `ImageGenError` carrying facts, not advice
|
|
130
|
+
([ADR-0006](./docs/adr/0006-library-errors-carry-facts-and-the-core-classifies-provider-failures.md)).
|
|
131
|
+
|
|
132
|
+
| Error | When |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `RefusalError`: `WrongModelKindError`, `UnsupportedRequestError` | before anything is sent, for a request that would fail silently: a shape, transparency, Mask or `quality` the model cannot give, an input over 50MB. `reason` names the cause |
|
|
135
|
+
| `RequestDegradedError`: `ProviderWarnedError`, `WrongSizeError` | the provider warned, or an exact `size` came back as another. `result` holds the billed images, and `failures` each position of the batch no image came back for, empty when none failed; `allowWarnings: true` returns the images instead, or throws `PartialResultError` when some failed ([ADR-0008](./docs/adr/0008-a-degraded-result-is-thrown-with-its-images.md), [ADR-0012](./docs/adr/0012-a-batch-exits-by-whoever-must-act-first.md)) |
|
|
136
|
+
| `PartialResultError` | some calls of a request for several images failed, or were aborted: `result` holds the ones that came back, `failures` each missing position with its failure or the abort, and `cause` the first of them ([ADR-0007](./docs/adr/0007-a-partial-result-is-thrown-with-the-images-that-came-back.md), [ADR-0012](./docs/adr/0012-a-batch-exits-by-whoever-must-act-first.md)) |
|
|
137
|
+
| `NoResultError` | every call of a request split over several failed or was aborted, so no image came back: `failures` holds each position with its failure or the abort, `cause` the first of them; a request sent as one call throws that call's own error instead ([ADR-0012](./docs/adr/0012-a-batch-exits-by-whoever-must-act-first.md)) |
|
|
138
|
+
| `ProviderFailedError` | the provider refused, could not be reached, or left an Unanswered Request, which may have been billed and is never resent ([ADR-0011](./docs/adr/0011-an-unanswered-request-is-never-resent.md)): `provider`, `answer`, and `reach`, one of `answered` (with `status` and `retryable`), `unreached`, `unanswered`, or `refreshUnanswered` (the plan's token refresh got no answer, before anything was billed) |
|
|
139
|
+
| `QuotaExhaustedError` | the plan's quota or an account's credit ran out: `provider`, `limit`, `resetsAt` |
|
|
140
|
+
| `LoginRequiredError` | from `@duyquangnvx/image-gen/codex`: the plan has no login; `command` is the fix |
|
|
141
|
+
| `LoginDeniedError` | from `@duyquangnvx/image-gen/codex`: the file system, often a sandbox, refuses to read the login, or to save a lapsed one's refresh in its directory `home`, which is checked before the refresh token is spent |
|
|
142
|
+
| `UnconfinableEditError`, `UnfittableImageError`, `UnsplittableImageError` | `confineTo()`, `fitToFrame()` or `splitPieces()` was given input it cannot work on; `reason` says why |
|
|
143
|
+
| `NoImageGeneratedError` | from `ai`: the model answered without an image, usually a content filter |
|
|
144
|
+
|
|
145
|
+
## Command line
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
image-gen generate "a knight in silver armour, full length" --out knight.png --transparent
|
|
149
|
+
cat prompt.txt | image-gen generate - --out hero.png --ref sketch.png --n 3 # hero-1.png … hero-3.png
|
|
150
|
+
image-gen edit knight.png "give him a red cape" --out caped.png --ref cape.png
|
|
151
|
+
image-gen edit hero.png "fix his face" --out hero-2.png --region 120,40,96,96 # the face changes; past a 12 px fade around it, nothing does
|
|
152
|
+
image-gen generate "a green frog" --out frog.png --model google:gemini-3.1-flash-image --cutout --backdrop '#ff00ff'
|
|
153
|
+
image-gen cutout frog.plate.png --out frog-2.png --contract 1 # re-key the plate, no model call
|
|
154
|
+
image-gen fit frog.png --frame 392x180 --out frog-392.png # no model call
|
|
155
|
+
image-gen split kit.png --out-dir pieces # kit-01.png, kit-02.png … one per Piece, no model call
|
|
156
|
+
image-gen contact-sheet frog.png frog-2.png --out cmp.png # compare in one look, no model call
|
|
157
|
+
image-gen doctor # the pinned model, every provider's key or login, and the latest release, billing nothing
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`image-gen <command> --help` lists each command's flags.
|
|
161
|
+
|
|
162
|
+
**Who pays.** `--model provider:id` picks the model and who is billed: `openai:<id>` bills
|
|
163
|
+
`OPENAI_API_KEY`, `codex:gpt-image-2` the ChatGPT plan, `google:<gemini-id>`
|
|
164
|
+
`GOOGLE_GENERATIVE_AI_API_KEY`. Keys are read from the shell, or else `~/.config/image-gen/.env`.
|
|
165
|
+
Without `--model`, a run takes the model pinned by the nearest `image-gen.json`
|
|
166
|
+
(`{ "model": "codex:gpt-image-2" }`); with neither, it is refused before anything is sent. Every
|
|
167
|
+
run's last line says who was billed. `openai` sends `--option quality=medium` unless told otherwise,
|
|
168
|
+
since each step up costs several times more; the plan picks its own quality.
|
|
169
|
+
|
|
170
|
+
**What it writes.** `--out` is always a PNG, and an existing file is refused before anything is
|
|
171
|
+
billed, unless `--force` replaces it or `--resume` keeps it. Each image a model call wrote gets a
|
|
172
|
+
Provenance Record beside it, `knight.png.json`, holding the full prompt, input hashes, model, options
|
|
173
|
+
and warnings; `--resume` keeps an image whose record says this run asked for it, and sends only for
|
|
174
|
+
the rest. Images billed before a failure are still written. Every `data.images` entry has the same
|
|
175
|
+
keys: `kept` is `true` for an image `--resume` kept and `false` for one this run billed, `provenance`
|
|
176
|
+
is `null` for a record that could not be written, and a `--cutout` plate whose keyed image could not
|
|
177
|
+
be written has `null` for everything but `plate` and `kept`.
|
|
178
|
+
|
|
179
|
+
**For agents.** `--json` prints exactly one envelope on stdout; a failure carries a stable `code`, a
|
|
180
|
+
`hint` naming the next command, and `details`. A plan call takes 30–145 s and gets up to 270 s, less when the
|
|
181
|
+
run's 9 minutes are nearly used, so that the whole run, writes included, ends within them; the
|
|
182
|
+
`E_NO_ANSWER` message gives the time the call actually had. A run with under 10 s left for its call
|
|
183
|
+
does not send it (`E_TIMEOUT`). Give the tool a timeout of 10 minutes.
|
|
184
|
+
Ctrl-C stops a run from sending more, but lets the calls already sent finish and writes their images.
|
|
185
|
+
[`skills/image-gen/`](./skills/image-gen/SKILL.md) is a Claude Code skill for driving the CLI; it
|
|
186
|
+
ships in the package, `image-gen skill install` copies it to
|
|
187
|
+
`~/.claude/skills/image-gen`, and `image-gen doctor` says whether that copy matches the installed CLI.
|
|
188
|
+
|
|
189
|
+
| Exit | Who acts (repo-harness ADR 0002) |
|
|
190
|
+
|---|---|
|
|
191
|
+
| 0 | done |
|
|
192
|
+
| 1 | the caller fixes its command (a flag, an argument, a file, a request the model refuses or degrades, a run whose 9 minutes passed, or left under 10 s for its call, before sending: `E_TIMEOUT`) |
|
|
193
|
+
| 2 | the operator acts on their machine (a key, `E_TOKEN_REJECTED`, the Plan Login, `E_OUT_OF_CREDIT`, the sandbox, a request that may have been billed with no answer back, within the time it had or at all: `E_NO_ANSWER`, or an `update` that npm, the registry's credentials or the new CLI's `skill install` refused) |
|
|
194
|
+
| 3 | someone at the source acts (`E_NO_LATEST`: the registry names no release to update to) |
|
|
195
|
+
| 70 | a bug in image-gen |
|
|
196
|
+
| 75 | nobody acts: wait, then run it again (the plan's quota, until `details.retryAt` when known; a provider failing for now, `E_HTTP_TRANSIENT`, a provider or the registry never reached, `E_NETWORK`, `E_REGISTRY_UNREACHABLE`; a plan token refresh that got no answer, `E_LOGIN_NO_ANSWER`) |
|
|
197
|
+
| 130, 143 | interrupted |
|
|
198
|
+
|
|
199
|
+
## Scripts
|
|
200
|
+
|
|
201
|
+
| | |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `pnpm test` | vitest, no network, no keys |
|
|
204
|
+
| `pnpm typecheck` | `tsc --noEmit` |
|
|
205
|
+
| `pnpm lint` | Biome, warnings fail |
|
|
206
|
+
| `pnpm build` | tsdown; also runs on `npm pack` via `prepack` |
|
|
207
|
+
| `pnpm e2e` | live, 5 Gemini calls: the three-tier character run |
|
|
208
|
+
| `pnpm e2e:codex` | live, 1 plan call: proves the route is up |
|
|
209
|
+
| `pnpm e2e:google` | live, 2 Gemini calls through the built CLI |
|
|
210
|
+
| `pnpm e2e:pipeline` | live, 6 plan calls: the reference pipeline |
|
|
211
|
+
| `pnpm release` | tags and publishes to npmjs: [`docs/agents/release.md`](./docs/agents/release.md) |
|
|
212
|
+
|
|
213
|
+
Gemini scripts need `GOOGLE_GENERATIVE_AI_API_KEY` in `.env.local`. Live scripts write to the
|
|
214
|
+
git-ignored `out/<timestamp>/`.
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|