studio-lumiere-cli 0.1.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.
Files changed (114) hide show
  1. package/.agents/skills/annotate-image/SKILL.md +99 -0
  2. package/.agents/skills/config-troubleshooting/SKILL.md +97 -0
  3. package/.agents/skills/generate-images/SKILL.md +667 -0
  4. package/.agents/skills/generate-video/SKILL.md +328 -0
  5. package/.agents/skills/image-grid/SKILL.md +96 -0
  6. package/.agents/skills/image-overlay/SKILL.md +66 -0
  7. package/.agents/skills/image-overlay/agents/openai.yaml +4 -0
  8. package/.agents/skills/image-overlay/scripts/overlay-image.js +218 -0
  9. package/.agents/skills/muse-management/SKILL.md +232 -0
  10. package/.agents/skills/refine-images/SKILL.md +192 -0
  11. package/.agents/skills/tired-girl/SKILL.md +131 -0
  12. package/.env.example +2 -0
  13. package/AGENTS.md +66 -0
  14. package/README.md +96 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +214 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/clients/geminiClient.d.ts +37 -0
  19. package/dist/clients/geminiClient.js +129 -0
  20. package/dist/clients/geminiClient.js.map +1 -0
  21. package/dist/config/constants.d.ts +63 -0
  22. package/dist/config/constants.js +1005 -0
  23. package/dist/config/constants.js.map +1 -0
  24. package/dist/config/options.d.ts +1 -0
  25. package/dist/config/options.js +2 -0
  26. package/dist/config/options.js.map +1 -0
  27. package/dist/config/templates.d.ts +3 -0
  28. package/dist/config/templates.js +4 -0
  29. package/dist/config/templates.js.map +1 -0
  30. package/dist/config/tiredGirl.d.ts +3 -0
  31. package/dist/config/tiredGirl.js +9 -0
  32. package/dist/config/tiredGirl.js.map +1 -0
  33. package/dist/image/annotate.d.ts +2 -0
  34. package/dist/image/annotate.js +119 -0
  35. package/dist/image/annotate.js.map +1 -0
  36. package/dist/image/grid.d.ts +2 -0
  37. package/dist/image/grid.js +44 -0
  38. package/dist/image/grid.js.map +1 -0
  39. package/dist/index.d.ts +12 -0
  40. package/dist/index.js +13 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/pipelines/createMuse.d.ts +3 -0
  43. package/dist/pipelines/createMuse.js +49 -0
  44. package/dist/pipelines/createMuse.js.map +1 -0
  45. package/dist/pipelines/generateImages.d.ts +2 -0
  46. package/dist/pipelines/generateImages.js +140 -0
  47. package/dist/pipelines/generateImages.js.map +1 -0
  48. package/dist/pipelines/generateTiredGirl.d.ts +2 -0
  49. package/dist/pipelines/generateTiredGirl.js +73 -0
  50. package/dist/pipelines/generateTiredGirl.js.map +1 -0
  51. package/dist/pipelines/generateVideo.d.ts +2 -0
  52. package/dist/pipelines/generateVideo.js +27 -0
  53. package/dist/pipelines/generateVideo.js.map +1 -0
  54. package/dist/pipelines/refineImage.d.ts +2 -0
  55. package/dist/pipelines/refineImage.js +28 -0
  56. package/dist/pipelines/refineImage.js.map +1 -0
  57. package/dist/pipelines/resolve.d.ts +11 -0
  58. package/dist/pipelines/resolve.js +74 -0
  59. package/dist/pipelines/resolve.js.map +1 -0
  60. package/dist/pipelines/upscaleImage.d.ts +2 -0
  61. package/dist/pipelines/upscaleImage.js +23 -0
  62. package/dist/pipelines/upscaleImage.js.map +1 -0
  63. package/dist/prompt/buildPrompt.d.ts +4 -0
  64. package/dist/prompt/buildPrompt.js +322 -0
  65. package/dist/prompt/buildPrompt.js.map +1 -0
  66. package/dist/prompt/tiredGirlPrompt.d.ts +3 -0
  67. package/dist/prompt/tiredGirlPrompt.js +33 -0
  68. package/dist/prompt/tiredGirlPrompt.js.map +1 -0
  69. package/dist/storage/files.d.ts +15 -0
  70. package/dist/storage/files.js +34 -0
  71. package/dist/storage/files.js.map +1 -0
  72. package/dist/storage/museStore.d.ts +5 -0
  73. package/dist/storage/museStore.js +26 -0
  74. package/dist/storage/museStore.js.map +1 -0
  75. package/dist/types.d.ts +169 -0
  76. package/dist/types.js +2 -0
  77. package/dist/types.js.map +1 -0
  78. package/examples/generate.d.ts +1 -0
  79. package/examples/generate.js +28 -0
  80. package/examples/generate.js.map +1 -0
  81. package/examples/generate.ts +30 -0
  82. package/examples/muse.d.ts +1 -0
  83. package/examples/muse.js +18 -0
  84. package/examples/muse.js.map +1 -0
  85. package/examples/muse.ts +20 -0
  86. package/examples/video.d.ts +1 -0
  87. package/examples/video.js +18 -0
  88. package/examples/video.js.map +1 -0
  89. package/examples/video.ts +20 -0
  90. package/logo-round.png +0 -0
  91. package/logo.jpeg +0 -0
  92. package/package.json +27 -0
  93. package/src/cli.ts +259 -0
  94. package/src/clients/geminiClient.ts +168 -0
  95. package/src/config/constants.ts +1105 -0
  96. package/src/config/options.ts +15 -0
  97. package/src/config/templates.ts +4 -0
  98. package/src/config/tiredGirl.ts +11 -0
  99. package/src/image/annotate.ts +139 -0
  100. package/src/image/grid.ts +58 -0
  101. package/src/index.ts +27 -0
  102. package/src/pipelines/createMuse.ts +76 -0
  103. package/src/pipelines/generateImages.ts +203 -0
  104. package/src/pipelines/generateTiredGirl.ts +86 -0
  105. package/src/pipelines/generateVideo.ts +36 -0
  106. package/src/pipelines/refineImage.ts +36 -0
  107. package/src/pipelines/resolve.ts +88 -0
  108. package/src/pipelines/upscaleImage.ts +30 -0
  109. package/src/prompt/buildPrompt.ts +380 -0
  110. package/src/prompt/tiredGirlPrompt.ts +35 -0
  111. package/src/storage/files.ts +41 -0
  112. package/src/storage/museStore.ts +31 -0
  113. package/src/types.ts +198 -0
  114. package/tsconfig.json +15 -0
package/src/cli.ts ADDED
@@ -0,0 +1,259 @@
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
+
@@ -0,0 +1,168 @@
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
+