studio-lumiere-cli 0.1.4 → 0.1.5
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/dist/config/constants.d.ts +1 -63
- package/dist/config/constants.js +3 -1004
- package/dist/config/constants.js.map +1 -1
- package/dist/pipelines/generateImages.js +5 -3
- package/dist/pipelines/generateImages.js.map +1 -1
- package/dist/storage/files.js +4 -1
- package/dist/storage/files.js.map +1 -1
- package/dist/studio/constants.d.ts +64 -0
- package/dist/studio/constants.js +1018 -0
- package/dist/studio/constants.js.map +1 -0
- package/dist/studio/promptBuilder.d.ts +4 -0
- package/{src/prompt/buildPrompt.ts → dist/studio/promptBuilder.js} +166 -221
- package/dist/studio/promptBuilder.js.map +1 -0
- package/dist/studio/types.d.ts +155 -0
- package/dist/studio/types.js +2 -0
- package/dist/studio/types.js.map +1 -0
- package/dist/types.d.ts +2 -49
- package/package.json +9 -3
- package/scripts/sync-studio.mjs +16 -0
- package/.agents/skills/generate-images/SKILL.md +0 -121
- package/.env.example +0 -2
- package/AGENTS.md +0 -66
- package/examples/generate.d.ts +0 -1
- package/examples/generate.js +0 -28
- package/examples/generate.js.map +0 -1
- package/examples/generate.ts +0 -30
- package/examples/muse.d.ts +0 -1
- package/examples/muse.js +0 -18
- package/examples/muse.js.map +0 -1
- package/examples/muse.ts +0 -20
- package/examples/video.d.ts +0 -1
- package/examples/video.js +0 -18
- package/examples/video.js.map +0 -1
- package/examples/video.ts +0 -20
- package/logo-round.png +0 -0
- package/logo.jpeg +0 -0
- package/skills/studio-lumiere-cli/SKILL.md +0 -212
- package/skills/studio-lumiere-cli/agents/openai.yaml +0 -4
- package/src/cli.ts +0 -259
- package/src/clients/geminiClient.ts +0 -168
- package/src/config/constants.ts +0 -1105
- package/src/config/options.ts +0 -15
- package/src/config/templates.ts +0 -4
- package/src/config/tiredGirl.ts +0 -11
- package/src/image/annotate.ts +0 -139
- package/src/image/grid.ts +0 -58
- package/src/index.ts +0 -27
- package/src/pipelines/createMuse.ts +0 -76
- package/src/pipelines/generateImages.ts +0 -203
- package/src/pipelines/generateTiredGirl.ts +0 -86
- package/src/pipelines/generateVideo.ts +0 -36
- package/src/pipelines/refineImage.ts +0 -36
- package/src/pipelines/resolve.ts +0 -88
- package/src/pipelines/upscaleImage.ts +0 -30
- package/src/prompt/tiredGirlPrompt.ts +0 -35
- package/src/storage/files.ts +0 -44
- package/src/storage/museStore.ts +0 -31
- package/src/types.ts +0 -198
- package/tsconfig.json +0 -15
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: studio-lumiere-cli
|
|
3
|
-
description: Command-line usage for Studio Lumiere. Use when running npx studio-lumiere-cli to generate, refine, upscale, create a Muse, generate video, tired-girl, annotate, grid, or list IDs.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Studio Lumiere CLI
|
|
7
|
-
|
|
8
|
-
## Requirements
|
|
9
|
-
- `GEMINI_API_KEY` must be set in your environment.
|
|
10
|
-
- `LUMIERE_OUTPUT_DIR` is optional (defaults to `outputs`).
|
|
11
|
-
|
|
12
|
-
## Invocation
|
|
13
|
-
`npx studio-lumiere-cli <command> [options]`
|
|
14
|
-
|
|
15
|
-
## Timing
|
|
16
|
-
Image and video generation can take time: expect ~1-2 minutes per image and ~4-5 minutes per video.
|
|
17
|
-
|
|
18
|
-
## list
|
|
19
|
-
Print JSON for valid IDs.
|
|
20
|
-
|
|
21
|
-
Example:
|
|
22
|
-
```bash
|
|
23
|
-
npx studio-lumiere-cli list templates
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Allowed types:
|
|
27
|
-
- `templates`
|
|
28
|
-
- `ethnicities`
|
|
29
|
-
- `skin-tones`
|
|
30
|
-
- `hair-colors`
|
|
31
|
-
- `backgrounds`
|
|
32
|
-
- `background-types`
|
|
33
|
-
- `vibes`
|
|
34
|
-
- `resolutions`
|
|
35
|
-
- `occasions`
|
|
36
|
-
|
|
37
|
-
## generate
|
|
38
|
-
Generate images from input jewelry photos.
|
|
39
|
-
|
|
40
|
-
Example:
|
|
41
|
-
```bash
|
|
42
|
-
npx studio-lumiere-cli generate \
|
|
43
|
-
--images ./inputs/ring.jpg \
|
|
44
|
-
--template hand_model \
|
|
45
|
-
--detail nail_nude \
|
|
46
|
-
--ethnicity mena \
|
|
47
|
-
--skin-tone medium \
|
|
48
|
-
--hair-color brunette \
|
|
49
|
-
--background cream_silk \
|
|
50
|
-
--background-type studio \
|
|
51
|
-
--vibe clean \
|
|
52
|
-
--resolution portrait \
|
|
53
|
-
--quantity 1
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Options:
|
|
57
|
-
- `--images <paths>` (required): Comma-separated input image paths.
|
|
58
|
-
- `--template <id>` (required): Template ID.
|
|
59
|
-
- `--detail <id>`
|
|
60
|
-
- `--secondary-detail <id>`
|
|
61
|
-
- `--tertiary-detail <id>`
|
|
62
|
-
- `--ethnicity <id>`
|
|
63
|
-
- `--skin-tone <id>`
|
|
64
|
-
- `--hair-color <id>`
|
|
65
|
-
- `--background <id>`
|
|
66
|
-
- `--background-type <id>`
|
|
67
|
-
- `--vibe <id>`
|
|
68
|
-
- `--resolution <id>`
|
|
69
|
-
- `--occasion <id>`
|
|
70
|
-
- `--quantity <n>`: Number of outputs (default `1`).
|
|
71
|
-
- `--muse-id <id>`: Use a stored Muse.
|
|
72
|
-
- `--muse-images <paths>`: Comma-separated Muse image paths.
|
|
73
|
-
- `--no-enhance`: Disable prompt enhancement.
|
|
74
|
-
|
|
75
|
-
Outputs:
|
|
76
|
-
Generated files are written to `outputs/generations/<timestamp>/` (or `LUMIERE_OUTPUT_DIR/generations/<timestamp>/`) with `image_#.png` and `generation.json`.
|
|
77
|
-
|
|
78
|
-
## refine
|
|
79
|
-
Refine an existing image.
|
|
80
|
-
|
|
81
|
-
Example:
|
|
82
|
-
```bash
|
|
83
|
-
npx studio-lumiere-cli refine \
|
|
84
|
-
--image ./outputs/image.png \
|
|
85
|
-
--instruction "Reduce glare and smooth background"
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Options:
|
|
89
|
-
- `--image <path>` (required)
|
|
90
|
-
- `--instruction <text>` (required)
|
|
91
|
-
- `--size <percent>`: Resize jewelry to a percentage of current size.
|
|
92
|
-
|
|
93
|
-
Outputs:
|
|
94
|
-
`outputs/refinements/<timestamp>/refined.png` and `refine.json`.
|
|
95
|
-
|
|
96
|
-
## upscale
|
|
97
|
-
Upscale an existing image.
|
|
98
|
-
|
|
99
|
-
Example:
|
|
100
|
-
```bash
|
|
101
|
-
npx studio-lumiere-cli upscale --image ./outputs/image.png --scale 2
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Options:
|
|
105
|
-
- `--image <path>` (required)
|
|
106
|
-
- `--scale <number>`: Scale factor (default `2`).
|
|
107
|
-
|
|
108
|
-
Outputs:
|
|
109
|
-
`outputs/upscales/<timestamp>/upscaled.png` and `upscale.json`.
|
|
110
|
-
|
|
111
|
-
## muse
|
|
112
|
-
Create a Muse from a source portrait.
|
|
113
|
-
|
|
114
|
-
Example:
|
|
115
|
-
```bash
|
|
116
|
-
npx studio-lumiere-cli muse --name "Nadine" --source ./inputs/portrait.jpg --variations 3
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Options:
|
|
120
|
-
- `--name <name>` (required)
|
|
121
|
-
- `--source <path>` (required)
|
|
122
|
-
- `--variations <n>`: Number of variations (default `3`).
|
|
123
|
-
|
|
124
|
-
Outputs:
|
|
125
|
-
`outputs/muses/<timestamp>/variation_#.png`, `muse.json`, and an updated `muses.json` under the output root.
|
|
126
|
-
|
|
127
|
-
## video
|
|
128
|
-
Generate a video from a prompt.
|
|
129
|
-
|
|
130
|
-
Example:
|
|
131
|
-
```bash
|
|
132
|
-
npx studio-lumiere-cli video --prompt "Slow pan over a diamond ring" --aspect 16:9 --duration 5
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Options:
|
|
136
|
-
- `--prompt <text>` (required)
|
|
137
|
-
- `--aspect <ratio>`
|
|
138
|
-
- `--duration <seconds>`
|
|
139
|
-
|
|
140
|
-
Outputs:
|
|
141
|
-
`outputs/videos/<timestamp>/video.mp4` (when available) and `video.json` with the operation name.
|
|
142
|
-
|
|
143
|
-
## tired-girl
|
|
144
|
-
Generate a tired-girl before image.
|
|
145
|
-
|
|
146
|
-
Example:
|
|
147
|
-
```bash
|
|
148
|
-
npx studio-lumiere-cli tired-girl --muse-id muse_1700000000000 --quantity 2
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Options:
|
|
152
|
-
- `--muse-id <id>`: Muse ID (preferred).
|
|
153
|
-
- `--image <path>`: Single reference image (used if no muse-id).
|
|
154
|
-
- `--styles <ids>`: Comma-separated style IDs.
|
|
155
|
-
- `--quantity <n>`: Number of variants (default `1`).
|
|
156
|
-
|
|
157
|
-
Outputs:
|
|
158
|
-
`outputs/tired_girl/<timestamp>/tired_girl_#.png` and `tired_girl.json`.
|
|
159
|
-
|
|
160
|
-
## annotate
|
|
161
|
-
Overlay text on an image.
|
|
162
|
-
|
|
163
|
-
Example:
|
|
164
|
-
```bash
|
|
165
|
-
npx studio-lumiere-cli annotate --input ./in.png --output ./out.png --text "Hello" --banner
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Options:
|
|
169
|
-
- `--input <path>` (required)
|
|
170
|
-
- `--output <path>` (required)
|
|
171
|
-
- `--text <text>` (required)
|
|
172
|
-
- `--font <name>`
|
|
173
|
-
- `--size <px>`
|
|
174
|
-
- `--weight <n>`
|
|
175
|
-
- `--color <hex>`
|
|
176
|
-
- `--stroke-color <value>`
|
|
177
|
-
- `--stroke-width <px>`
|
|
178
|
-
- `--banner`
|
|
179
|
-
- `--banner-color <value>`
|
|
180
|
-
- `--banner-padding <px>`
|
|
181
|
-
- `--banner-radius <px>`
|
|
182
|
-
- `--position <pos>`: `top-center|top-left|top-right|bottom-center|bottom-center-high|bottom-left|bottom-right|center`
|
|
183
|
-
- `--padding <px>`
|
|
184
|
-
- `--max-width <ratio>`
|
|
185
|
-
|
|
186
|
-
Outputs:
|
|
187
|
-
Writes the image to the provided `--output` path.
|
|
188
|
-
|
|
189
|
-
## grid
|
|
190
|
-
Assemble multiple images into a grid.
|
|
191
|
-
|
|
192
|
-
Example:
|
|
193
|
-
```bash
|
|
194
|
-
npx studio-lumiere-cli grid \
|
|
195
|
-
--inputs ./a.png,./b.png,./c.png,./d.png \
|
|
196
|
-
--output ./grid.png \
|
|
197
|
-
--columns 2 \
|
|
198
|
-
--rows 2
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
Options:
|
|
202
|
-
- `--inputs <paths>` (required): Comma-separated image paths.
|
|
203
|
-
- `--output <path>` (required)
|
|
204
|
-
- `--columns <n>` (default `2`)
|
|
205
|
-
- `--rows <n>` (default `2`)
|
|
206
|
-
- `--padding <px>` (default `20`)
|
|
207
|
-
- `--background <hex>` (default `#000000`)
|
|
208
|
-
- `--tile-width <px>`
|
|
209
|
-
- `--tile-height <px>`
|
|
210
|
-
|
|
211
|
-
Outputs:
|
|
212
|
-
Writes the image to the provided `--output` path.
|
package/src/cli.ts
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import "dotenv/config";
|
|
3
|
-
import { Command } from "commander";
|
|
4
|
-
import {
|
|
5
|
-
generateImages,
|
|
6
|
-
refineImage,
|
|
7
|
-
upscaleImage,
|
|
8
|
-
generateTiredGirl,
|
|
9
|
-
createMuse,
|
|
10
|
-
generateVideo,
|
|
11
|
-
annotateImage,
|
|
12
|
-
createImageGrid,
|
|
13
|
-
TEMPLATES,
|
|
14
|
-
ETHNICITIES,
|
|
15
|
-
SKIN_TONES,
|
|
16
|
-
HAIR_COLORS,
|
|
17
|
-
BACKGROUNDS,
|
|
18
|
-
BACKGROUND_TYPES,
|
|
19
|
-
VIBES,
|
|
20
|
-
RESOLUTIONS,
|
|
21
|
-
OCCASIONS
|
|
22
|
-
} from "./index.js";
|
|
23
|
-
import { LumiereConfig } from "./types.js";
|
|
24
|
-
|
|
25
|
-
const program = new Command();
|
|
26
|
-
|
|
27
|
-
const parseList = (value: string): string[] => value.split(",").map((v) => v.trim()).filter(Boolean);
|
|
28
|
-
|
|
29
|
-
const loadConfig = (): LumiereConfig => {
|
|
30
|
-
const apiKey = process.env.GEMINI_API_KEY;
|
|
31
|
-
if (!apiKey) {
|
|
32
|
-
throw new Error("Missing GEMINI_API_KEY in environment.");
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
apiKey,
|
|
36
|
-
outputDir: process.env.LUMIERE_OUTPUT_DIR ?? "outputs"
|
|
37
|
-
};
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
program
|
|
41
|
-
.name("lumiere")
|
|
42
|
-
.description("Local Studio Lumiere SDK + CLI")
|
|
43
|
-
.version("0.1.0");
|
|
44
|
-
|
|
45
|
-
program
|
|
46
|
-
.command("list")
|
|
47
|
-
.argument("type", "templates|ethnicities|skin-tones|hair-colors|backgrounds|background-types|vibes|resolutions|occasions")
|
|
48
|
-
.action((type) => {
|
|
49
|
-
const map: Record<string, unknown> = {
|
|
50
|
-
templates: TEMPLATES,
|
|
51
|
-
ethnicities: ETHNICITIES,
|
|
52
|
-
"skin-tones": SKIN_TONES,
|
|
53
|
-
"hair-colors": HAIR_COLORS,
|
|
54
|
-
backgrounds: BACKGROUNDS,
|
|
55
|
-
"background-types": BACKGROUND_TYPES,
|
|
56
|
-
vibes: VIBES,
|
|
57
|
-
resolutions: RESOLUTIONS,
|
|
58
|
-
occasions: OCCASIONS
|
|
59
|
-
};
|
|
60
|
-
const payload = map[type];
|
|
61
|
-
if (!payload) {
|
|
62
|
-
throw new Error(`Unknown list type: ${type}`);
|
|
63
|
-
}
|
|
64
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
program
|
|
68
|
-
.command("generate")
|
|
69
|
-
.requiredOption("--images <paths>", "Comma-separated image paths")
|
|
70
|
-
.requiredOption("--template <id>", "Template id")
|
|
71
|
-
.option("--detail <id>")
|
|
72
|
-
.option("--secondary-detail <id>")
|
|
73
|
-
.option("--tertiary-detail <id>")
|
|
74
|
-
.option("--ethnicity <id>")
|
|
75
|
-
.option("--skin-tone <id>")
|
|
76
|
-
.option("--hair-color <id>")
|
|
77
|
-
.option("--background <id>")
|
|
78
|
-
.option("--background-type <id>")
|
|
79
|
-
.option("--vibe <id>")
|
|
80
|
-
.option("--resolution <id>")
|
|
81
|
-
.option("--occasion <id>")
|
|
82
|
-
.option("--quantity <n>", "Number of images", "1")
|
|
83
|
-
.option("--muse-id <id>")
|
|
84
|
-
.option("--muse-images <paths>", "Comma-separated muse image paths")
|
|
85
|
-
.option("--no-enhance", "Skip prompt enhancement")
|
|
86
|
-
.action(async (options) => {
|
|
87
|
-
const config = loadConfig();
|
|
88
|
-
const result = await generateImages(config, {
|
|
89
|
-
inputImages: parseList(options.images),
|
|
90
|
-
quantity: Number(options.quantity),
|
|
91
|
-
selections: {
|
|
92
|
-
templateId: options.template,
|
|
93
|
-
detailId: options.detail,
|
|
94
|
-
secondaryDetailId: options.secondaryDetail,
|
|
95
|
-
tertiaryDetailId: options.tertiaryDetail,
|
|
96
|
-
ethnicityId: options.ethnicity,
|
|
97
|
-
skinToneId: options.skinTone,
|
|
98
|
-
hairColorId: options.hairColor,
|
|
99
|
-
backgroundId: options.background,
|
|
100
|
-
backgroundTypeId: options.backgroundType,
|
|
101
|
-
vibeId: options.vibe,
|
|
102
|
-
resolutionId: options.resolution,
|
|
103
|
-
occasionId: options.occasion
|
|
104
|
-
},
|
|
105
|
-
museId: options.museId,
|
|
106
|
-
museImagePaths: options.museImages ? parseList(options.museImages) : undefined,
|
|
107
|
-
enhancePrompt: options.enhance
|
|
108
|
-
});
|
|
109
|
-
console.log(JSON.stringify(result, null, 2));
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
program
|
|
113
|
-
.command("refine")
|
|
114
|
-
.requiredOption("--image <path>")
|
|
115
|
-
.requiredOption("--instruction <text>")
|
|
116
|
-
.option("--size <percent>")
|
|
117
|
-
.action(async (options) => {
|
|
118
|
-
const config = loadConfig();
|
|
119
|
-
const output = await refineImage(config, {
|
|
120
|
-
inputImage: options.image,
|
|
121
|
-
instruction: options.instruction,
|
|
122
|
-
sizeAdjustment: options.size ? Number(options.size) : undefined
|
|
123
|
-
});
|
|
124
|
-
console.log(output);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
program
|
|
128
|
-
.command("upscale")
|
|
129
|
-
.requiredOption("--image <path>")
|
|
130
|
-
.option("--scale <number>", "Scale factor", "2")
|
|
131
|
-
.action(async (options) => {
|
|
132
|
-
const config = loadConfig();
|
|
133
|
-
const output = await upscaleImage(config, {
|
|
134
|
-
inputImage: options.image,
|
|
135
|
-
scale: Number(options.scale)
|
|
136
|
-
});
|
|
137
|
-
console.log(output);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
program
|
|
141
|
-
.command("muse")
|
|
142
|
-
.requiredOption("--name <name>")
|
|
143
|
-
.requiredOption("--source <path>")
|
|
144
|
-
.option("--variations <n>", "Number of variations", "3")
|
|
145
|
-
.action(async (options) => {
|
|
146
|
-
const config = loadConfig();
|
|
147
|
-
const result = await createMuse(config, {
|
|
148
|
-
name: options.name,
|
|
149
|
-
sourceImage: options.source,
|
|
150
|
-
variations: Number(options.variations)
|
|
151
|
-
});
|
|
152
|
-
console.log(JSON.stringify(result, null, 2));
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
program
|
|
156
|
-
.command("video")
|
|
157
|
-
.requiredOption("--prompt <text>")
|
|
158
|
-
.option("--aspect <ratio>")
|
|
159
|
-
.option("--duration <seconds>")
|
|
160
|
-
.action(async (options) => {
|
|
161
|
-
const config = loadConfig();
|
|
162
|
-
const result = await generateVideo(config, {
|
|
163
|
-
prompt: options.prompt,
|
|
164
|
-
aspectRatio: options.aspect,
|
|
165
|
-
durationSeconds: options.duration ? Number(options.duration) : undefined
|
|
166
|
-
});
|
|
167
|
-
console.log(JSON.stringify(result, null, 2));
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
program
|
|
171
|
-
.command("tired-girl")
|
|
172
|
-
.option("--muse-id <id>", "Muse ID (preferred)")
|
|
173
|
-
.option("--image <path>", "Single reference image (used if no muse-id)")
|
|
174
|
-
.option("--styles <ids>", "Comma-separated style IDs")
|
|
175
|
-
.option("--quantity <n>", "Number of variants", "1")
|
|
176
|
-
.action(async (options) => {
|
|
177
|
-
const config = loadConfig();
|
|
178
|
-
const result = await generateTiredGirl(config, {
|
|
179
|
-
museId: options.museId,
|
|
180
|
-
inputImage: options.image,
|
|
181
|
-
styleIds: options.styles ? parseList(options.styles) : undefined,
|
|
182
|
-
quantity: Number(options.quantity)
|
|
183
|
-
});
|
|
184
|
-
console.log(JSON.stringify(result, null, 2));
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
program
|
|
188
|
-
.command("annotate")
|
|
189
|
-
.requiredOption("--input <path>", "Input image path")
|
|
190
|
-
.requiredOption("--output <path>", "Output image path")
|
|
191
|
-
.requiredOption("--text <text>", "Overlay text")
|
|
192
|
-
.option("--font <name>", "Font family (default: Poppins)")
|
|
193
|
-
.option("--size <px>", "Font size in px")
|
|
194
|
-
.option("--weight <n>", "Font weight (default: 700)")
|
|
195
|
-
.option("--color <hex>", "Text color (default: #FFFFFF)")
|
|
196
|
-
.option("--stroke-color <value>", "Stroke/outline color (default: rgba(0,0,0,0.6))")
|
|
197
|
-
.option("--stroke-width <px>", "Stroke/outline width (default: fontSize * 0.08)")
|
|
198
|
-
.option("--banner", "Enable semi-transparent banner behind text")
|
|
199
|
-
.option("--banner-color <value>", "Banner color (default: rgba(0,0,0,0.45))")
|
|
200
|
-
.option("--banner-padding <px>", "Banner padding (default: fontSize * 0.6)")
|
|
201
|
-
.option("--banner-radius <px>", "Banner corner radius (default: fontSize * 0.4)")
|
|
202
|
-
.option(
|
|
203
|
-
"--position <pos>",
|
|
204
|
-
"top-center|top-left|top-right|bottom-center|bottom-center-high|bottom-left|bottom-right|center"
|
|
205
|
-
)
|
|
206
|
-
.option("--padding <px>", "Padding from edges")
|
|
207
|
-
.option("--max-width <ratio>", "Max width ratio (0-1)")
|
|
208
|
-
.action(async (options) => {
|
|
209
|
-
await annotateImage(
|
|
210
|
-
options.input,
|
|
211
|
-
options.output,
|
|
212
|
-
options.text,
|
|
213
|
-
{
|
|
214
|
-
fontFamily: options.font,
|
|
215
|
-
fontSize: options.size ? Number(options.size) : undefined,
|
|
216
|
-
fontWeight: options.weight ? Number(options.weight) : undefined,
|
|
217
|
-
color: options.color,
|
|
218
|
-
strokeColor: options.strokeColor,
|
|
219
|
-
strokeWidth: options.strokeWidth ? Number(options.strokeWidth) : undefined,
|
|
220
|
-
banner: options.banner ?? false,
|
|
221
|
-
bannerColor: options.bannerColor,
|
|
222
|
-
bannerPadding: options.bannerPadding ? Number(options.bannerPadding) : undefined,
|
|
223
|
-
bannerRadius: options.bannerRadius ? Number(options.bannerRadius) : undefined,
|
|
224
|
-
position: options.position,
|
|
225
|
-
padding: options.padding ? Number(options.padding) : undefined,
|
|
226
|
-
maxWidthRatio: options.maxWidth ? Number(options.maxWidth) : undefined
|
|
227
|
-
}
|
|
228
|
-
);
|
|
229
|
-
console.log(options.output);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
program
|
|
233
|
-
.command("grid")
|
|
234
|
-
.requiredOption("--inputs <paths>", "Comma-separated image paths")
|
|
235
|
-
.requiredOption("--output <path>", "Output image path")
|
|
236
|
-
.option("--columns <n>", "Number of columns", "2")
|
|
237
|
-
.option("--rows <n>", "Number of rows", "2")
|
|
238
|
-
.option("--padding <px>", "Padding between tiles (default: 20)")
|
|
239
|
-
.option("--background <hex>", "Background color (default: #000000)")
|
|
240
|
-
.option("--tile-width <px>", "Fixed tile width")
|
|
241
|
-
.option("--tile-height <px>", "Fixed tile height")
|
|
242
|
-
.action(async (options) => {
|
|
243
|
-
await createImageGrid(
|
|
244
|
-
parseList(options.inputs),
|
|
245
|
-
options.output,
|
|
246
|
-
{
|
|
247
|
-
columns: Number(options.columns),
|
|
248
|
-
rows: Number(options.rows),
|
|
249
|
-
padding: options.padding ? Number(options.padding) : undefined,
|
|
250
|
-
background: options.background,
|
|
251
|
-
tileWidth: options.tileWidth ? Number(options.tileWidth) : undefined,
|
|
252
|
-
tileHeight: options.tileHeight ? Number(options.tileHeight) : undefined
|
|
253
|
-
}
|
|
254
|
-
);
|
|
255
|
-
console.log(options.output);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
program.parseAsync();
|
|
259
|
-
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { GoogleGenAI } from "@google/genai";
|
|
2
|
-
import { AspectRatio, LumiereConfig } from "../types.js";
|
|
3
|
-
|
|
4
|
-
type RetryConfig = {
|
|
5
|
-
maxRetries: number;
|
|
6
|
-
baseDelayMs: number;
|
|
7
|
-
maxDelayMs: number;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
type ModelConfig = {
|
|
11
|
-
prompt: string;
|
|
12
|
-
image: string;
|
|
13
|
-
video: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
-
|
|
18
|
-
const isRetryableError = (error: unknown): boolean => {
|
|
19
|
-
const message = String((error as any)?.message || "").toLowerCase();
|
|
20
|
-
return (
|
|
21
|
-
message.includes("429") ||
|
|
22
|
-
message.includes("503") ||
|
|
23
|
-
message.includes("overloaded") ||
|
|
24
|
-
message.includes("unavailable") ||
|
|
25
|
-
message.includes("timeout")
|
|
26
|
-
);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const withRetry = async <T>(fn: () => Promise<T>, config: RetryConfig): Promise<T> => {
|
|
30
|
-
let lastError: unknown;
|
|
31
|
-
for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {
|
|
32
|
-
try {
|
|
33
|
-
return await fn();
|
|
34
|
-
} catch (error) {
|
|
35
|
-
lastError = error;
|
|
36
|
-
if (attempt === config.maxRetries || !isRetryableError(error)) {
|
|
37
|
-
throw error;
|
|
38
|
-
}
|
|
39
|
-
const delay = Math.min(config.baseDelayMs * 2 ** attempt, config.maxDelayMs);
|
|
40
|
-
await sleep(delay + Math.random() * 0.3 * delay);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
throw lastError;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export class GeminiClient {
|
|
47
|
-
private ai: GoogleGenAI;
|
|
48
|
-
private models: ModelConfig;
|
|
49
|
-
private retry: RetryConfig;
|
|
50
|
-
|
|
51
|
-
constructor(config: LumiereConfig) {
|
|
52
|
-
this.ai = new GoogleGenAI({ apiKey: config.apiKey });
|
|
53
|
-
this.models = {
|
|
54
|
-
prompt: config.models?.prompt ?? "gemini-3-flash-preview",
|
|
55
|
-
image: config.models?.image ?? "gemini-3-pro-image-preview",
|
|
56
|
-
video: config.models?.video ?? "veo-3.1-generate-preview"
|
|
57
|
-
};
|
|
58
|
-
this.retry = {
|
|
59
|
-
maxRetries: config.retry?.maxRetries ?? 3,
|
|
60
|
-
baseDelayMs: config.retry?.baseDelayMs ?? 1500,
|
|
61
|
-
maxDelayMs: config.retry?.maxDelayMs ?? 12000
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async generateText(prompt: string): Promise<string> {
|
|
66
|
-
return withRetry(async () => {
|
|
67
|
-
const response = await this.ai.models.generateContent({
|
|
68
|
-
model: this.models.prompt,
|
|
69
|
-
contents: prompt
|
|
70
|
-
});
|
|
71
|
-
return response.text ?? "";
|
|
72
|
-
}, this.retry);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async generateEnhancedPrompt(params: {
|
|
76
|
-
parts: Array<{ text?: string; inlineData?: { mimeType: string; data: string } }>;
|
|
77
|
-
systemInstruction?: string;
|
|
78
|
-
}): Promise<string> {
|
|
79
|
-
return withRetry(async () => {
|
|
80
|
-
const response = await this.ai.models.generateContent({
|
|
81
|
-
model: this.models.prompt,
|
|
82
|
-
contents: [
|
|
83
|
-
{
|
|
84
|
-
role: "user",
|
|
85
|
-
parts: params.parts
|
|
86
|
-
}
|
|
87
|
-
],
|
|
88
|
-
config: {
|
|
89
|
-
systemInstruction: params.systemInstruction
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
return response.text ?? "";
|
|
93
|
-
}, this.retry);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async generateImage(params: {
|
|
97
|
-
parts: Array<{ text?: string; inlineData?: { mimeType: string; data: string } }>;
|
|
98
|
-
aspectRatio?: AspectRatio;
|
|
99
|
-
imageSize?: "1K" | "2K" | "4K";
|
|
100
|
-
}): Promise<string[]> {
|
|
101
|
-
return withRetry(async () => {
|
|
102
|
-
const response = await this.ai.models.generateContent({
|
|
103
|
-
model: this.models.image,
|
|
104
|
-
contents: [
|
|
105
|
-
{
|
|
106
|
-
role: "user",
|
|
107
|
-
parts: params.parts
|
|
108
|
-
}
|
|
109
|
-
],
|
|
110
|
-
config: {
|
|
111
|
-
imageConfig: {
|
|
112
|
-
aspectRatio: params.aspectRatio,
|
|
113
|
-
imageSize: params.imageSize ?? "2K"
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const images: string[] = [];
|
|
119
|
-
const parts = response.candidates?.[0]?.content?.parts ?? [];
|
|
120
|
-
for (const part of parts) {
|
|
121
|
-
const inlineData = (part as any)?.inlineData;
|
|
122
|
-
if (inlineData?.data) {
|
|
123
|
-
images.push(`data:${inlineData.mimeType ?? "image/png"};base64,${inlineData.data}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (images.length === 0) {
|
|
127
|
-
throw new Error("No image data returned from Gemini.");
|
|
128
|
-
}
|
|
129
|
-
return images;
|
|
130
|
-
}, this.retry);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async generateVideo(params: {
|
|
134
|
-
prompt: string;
|
|
135
|
-
aspectRatio?: AspectRatio;
|
|
136
|
-
durationSeconds?: number;
|
|
137
|
-
}): Promise<{ operationName?: string; videoBytes?: Uint8Array }>{
|
|
138
|
-
return withRetry(async () => {
|
|
139
|
-
let operation = await this.ai.models.generateVideos({
|
|
140
|
-
model: this.models.video,
|
|
141
|
-
prompt: params.prompt,
|
|
142
|
-
config: {
|
|
143
|
-
aspectRatio: params.aspectRatio,
|
|
144
|
-
durationSeconds: params.durationSeconds
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
while (!operation.done) {
|
|
149
|
-
await sleep(10000);
|
|
150
|
-
operation = await this.ai.operations.getVideosOperation({ operation });
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const generated = operation.response?.generatedVideos?.[0]?.video;
|
|
154
|
-
if (!generated) {
|
|
155
|
-
return { operationName: operation.name };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const rawBytes = (generated as any).videoBytes;
|
|
159
|
-
let videoBytes: Uint8Array | undefined;
|
|
160
|
-
if (rawBytes) {
|
|
161
|
-
videoBytes = typeof rawBytes === "string" ? Buffer.from(rawBytes, "base64") : rawBytes;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return { operationName: operation.name, videoBytes };
|
|
165
|
-
}, this.retry);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|