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
@@ -0,0 +1,328 @@
1
+ ---
2
+ name: generate-video
3
+ description: End-to-end guide to generate video with local-lumiere SDK/CLI; use for video generation workflows and parameter reference.
4
+ ---
5
+
6
+ # Skill: Generate Video (local-lumiere)
7
+
8
+ This document is a complete, end-to-end guide for an AI agent to generate video using the **local-lumiere** SDK/CLI. It is designed to be consumed as a standalone skill: read this file, then follow the steps exactly. It covers **every function**, **every parameter**, and **all configuration points** involved in video generation.
9
+
10
+ ---
11
+
12
+ ## Quick Start (60 seconds)
13
+
14
+ 1) Ensure `GEMINI_API_KEY` is set in your environment.
15
+ 2) Build the project:
16
+
17
+ ```
18
+ npm install
19
+ npm run build
20
+ ```
21
+
22
+ 3) Generate a video via CLI:
23
+
24
+ ```
25
+ node dist/cli.js video \
26
+ --prompt "A cinematic close-up of a model gently turning her head, earrings catching soft light." \
27
+ --aspect 9:16 \
28
+ --duration 5
29
+ ```
30
+
31
+ 4) Find output files under `outputs/videos/<timestamp>/`.
32
+
33
+ ---
34
+
35
+ ## 0) Purpose
36
+
37
+ The goal is to generate short, high-end jewelry videos locally using Gemini/Veo via the SDK. This skill focuses on **video generation** only. It does not cover image generation, refinement, upscaling, or Muse creation (except for references in prompt crafting).
38
+
39
+ ---
40
+
41
+ ## 1) Where the Video Generation API Lives
42
+
43
+ **SDK entrypoint**: `src/index.ts`
44
+
45
+ - `generateVideo` (primary API)
46
+
47
+ **Pipeline implementation**: `src/pipelines/generateVideo.ts`
48
+
49
+ **Gemini client wrapper**: `src/clients/geminiClient.ts`
50
+
51
+ **File I/O**: `src/storage/files.ts`
52
+
53
+ ---
54
+
55
+ ## 2) Required Environment and Configuration
56
+
57
+ ### 2.1 Environment Variables
58
+
59
+ - `GEMINI_API_KEY` (required)
60
+ - Used by `GeminiClient` to authenticate to Gemini APIs.
61
+
62
+ - `LUMIERE_OUTPUT_DIR` (optional)
63
+ - Default output directory for all generated assets.
64
+ - Defaults to `outputs` if not set.
65
+
66
+ ### 2.2 Runtime Config Object
67
+
68
+ All SDK functions expect a `LumiereConfig` object:
69
+
70
+ ```ts
71
+ export interface LumiereConfig {
72
+ apiKey: string; // required
73
+ outputDir: string; // required (can be read from env)
74
+ models?: {
75
+ prompt?: string; // Gemini text model
76
+ image?: string; // Gemini image model
77
+ video?: string; // Gemini video model
78
+ };
79
+ retry?: {
80
+ maxRetries?: number; // default: 3
81
+ baseDelayMs?: number;// default: 1500
82
+ maxDelayMs?: number; // default: 12000
83
+ };
84
+ }
85
+ ```
86
+
87
+ Defaults if omitted:
88
+
89
+ - `models.prompt`: `gemini-3-flash-preview`
90
+ - `models.image`: `gemini-3-flash-preview-image`
91
+ - `models.video`: `veo-3.1-generate-preview`
92
+ - `retry.maxRetries`: 3
93
+ - `retry.baseDelayMs`: 1500
94
+ - `retry.maxDelayMs`: 12000
95
+
96
+ ### 2.3 Output Directory Structure
97
+
98
+ `generateVideo()` creates a timestamped folder under `outputDir`:
99
+
100
+ ```
101
+ outputs/
102
+ videos/
103
+ 2026-02-11T19-40-12-123Z/
104
+ video.mp4
105
+ video.json
106
+ ```
107
+
108
+ The timestamp is generated in `resolveOutputDir()` in `src/storage/files.ts`.
109
+
110
+ ---
111
+
112
+ ## 3) Primary Function: generateVideo
113
+
114
+ ### 3.1 Function Signature
115
+
116
+ ```ts
117
+ export const generateVideo = async (
118
+ config: LumiereConfig,
119
+ request: VideoRequest
120
+ ): Promise<VideoResult>
121
+ ```
122
+
123
+ ### 3.2 Request Object: VideoRequest
124
+
125
+ ```ts
126
+ export interface VideoRequest {
127
+ prompt: string; // Required. Full video prompt.
128
+ imageInput?: string; // Optional. Reserved for future use (not currently used).
129
+ durationSeconds?: number; // Optional. Duration in seconds.
130
+ aspectRatio?: AspectRatio; // Optional. "1:1" | "3:4" | "4:3" | "9:16" | "16:9"
131
+ outputDir?: string; // Optional. Override config.outputDir.
132
+ }
133
+ ```
134
+
135
+ ### 3.3 Response Object: VideoResult
136
+
137
+ ```ts
138
+ export interface VideoResult {
139
+ operationName?: string; // Operation name from Gemini/Veo
140
+ videoPath?: string; // Path to saved .mp4 if available
141
+ logPath: string; // Path to video.json log
142
+ }
143
+ ```
144
+
145
+ ---
146
+
147
+ ## 4) Gemini Video Generation Call
148
+
149
+ Video generation is performed by:
150
+
151
+ ```ts
152
+ GeminiClient.generateVideo({
153
+ prompt,
154
+ aspectRatio,
155
+ durationSeconds
156
+ })
157
+ ```
158
+
159
+ ### 4.1 Prompt
160
+
161
+ The prompt should be **complete and explicit** about:
162
+
163
+ - Subject (model/jewelry)
164
+ - Motion and camera movement
165
+ - Lighting and mood
166
+ - Composition and framing
167
+
168
+ Example:
169
+
170
+ "A cinematic close-up of a model gently turning her head, earrings catching soft light. High-end jewelry commercial aesthetic. Slow, elegant motion. Shallow depth of field."
171
+
172
+ ### 4.2 Aspect Ratio
173
+
174
+ Optional. Must be one of:
175
+
176
+ - `1:1`
177
+ - `3:4`
178
+ - `4:3`
179
+ - `9:16`
180
+ - `16:9`
181
+
182
+ ### 4.3 Duration
183
+
184
+ Optional. Duration in seconds (integer). If omitted, the provider default is used.
185
+
186
+ ### 4.4 Long-Running Operation
187
+
188
+ `GeminiClient.generateVideo()` polls the operation until completion:
189
+
190
+ - Starts with `ai.models.generateVideos()`
191
+ - Polls using `ai.operations.getVideosOperation()`
192
+ - Returns `operationName` and `videoBytes` if available
193
+
194
+ ---
195
+
196
+ ## 5) Output Saving and Logging
197
+
198
+ When the operation completes and video bytes are available:
199
+
200
+ - The file is saved as `video.mp4` in the output folder
201
+ - A JSON log is written as `video.json`
202
+
203
+ Example log:
204
+
205
+ ```json
206
+ {
207
+ "prompt": "A cinematic close-up...",
208
+ "aspectRatio": "9:16",
209
+ "durationSeconds": 5,
210
+ "operationName": "operations/abc123",
211
+ "videoPath": "outputs/videos/2026-02-11.../video.mp4"
212
+ }
213
+ ```
214
+
215
+ If no video bytes are returned, `videoPath` will be omitted but the log still records `operationName`.
216
+
217
+ ---
218
+
219
+ ## 6) CLI Usage (Full Parameters)
220
+
221
+ The CLI is implemented in `src/cli.ts` and compiled to `dist/cli.js`.
222
+
223
+ ### 6.1 Video Command
224
+
225
+ ```
226
+ node dist/cli.js video \
227
+ --prompt "Cinematic head turn showing earrings" \
228
+ --aspect 9:16 \
229
+ --duration 5
230
+ ```
231
+
232
+ ### 6.2 CLI Flags
233
+
234
+ - `--prompt`: **required**
235
+ - `--aspect`: optional (aspect ratio string)
236
+ - `--duration`: optional (seconds)
237
+
238
+ ---
239
+
240
+ ## 7) Function Call Example (SDK)
241
+
242
+ ```ts
243
+ import { generateVideo } from "./dist/index.js";
244
+
245
+ const result = await generateVideo(
246
+ {
247
+ apiKey: process.env.GEMINI_API_KEY!,
248
+ outputDir: "outputs",
249
+ models: {
250
+ video: "veo-3.1-generate-preview"
251
+ },
252
+ retry: {
253
+ maxRetries: 3,
254
+ baseDelayMs: 1500,
255
+ maxDelayMs: 12000
256
+ }
257
+ },
258
+ {
259
+ prompt: "A cinematic close-up of a model gently turning her head, earrings catching soft light.",
260
+ aspectRatio: "9:16",
261
+ durationSeconds: 5
262
+ }
263
+ );
264
+
265
+ console.log(result);
266
+ ```
267
+
268
+ ---
269
+
270
+ ## 8) Error Handling and Retries
271
+
272
+ All Gemini calls use a retry wrapper:
273
+
274
+ ```ts
275
+ retry: {
276
+ maxRetries: 3,
277
+ baseDelayMs: 1500,
278
+ maxDelayMs: 12000
279
+ }
280
+ ```
281
+
282
+ Retryable error detection looks for:
283
+ - HTTP 429 / 503
284
+ - “overloaded”, “unavailable”, “timeout” text
285
+
286
+ If all retries fail, the error is thrown as-is.
287
+
288
+ ---
289
+
290
+ ## 9) Extending or Modifying the Skill
291
+
292
+ If you want a different video model or output duration defaults:
293
+
294
+ - Override `config.models.video` when calling `generateVideo`.
295
+ - Modify `src/clients/geminiClient.ts` for custom polling intervals or defaults.
296
+ - Update the CLI to expose additional parameters.
297
+
298
+ ---
299
+
300
+ ## 10) Quick Checklist for an AI Agent
301
+
302
+ 1) Ensure `GEMINI_API_KEY` is set.
303
+ 2) Write a clear, cinematic prompt (subject, motion, lighting, mood).
304
+ 3) Decide aspect ratio and duration.
305
+ 4) Call `generateVideo(config, request)` or use the CLI.
306
+ 5) Read output paths and logs from the result.
307
+
308
+ ---
309
+
310
+ ## 11) Related Files (For Reference)
311
+
312
+ - `src/pipelines/generateVideo.ts`
313
+ - `src/clients/geminiClient.ts`
314
+ - `src/storage/files.ts`
315
+ - `src/cli.ts`
316
+
317
+ End of skill.
318
+
319
+ ## Related Skills
320
+
321
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\generate-images.md`
322
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\generate-video.md`
323
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\refine-images.md`
324
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\muse-management.md`
325
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\tired-girl.md`
326
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\image-grid.md`
327
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\config-troubleshooting.md`
328
+
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: image-grid
3
+ description: Assemble multiple images into a grid using the local-lumiere SDK/CLI; use when you need tiled or collage layouts.
4
+ ---
5
+
6
+ # Skill: Image Grid Assembly (local-lumiere)
7
+
8
+ This document explains how to assemble multiple images into a grid using the **local-lumiere** SDK/CLI.
9
+
10
+ ---
11
+
12
+ ## Quick Start (60 seconds)
13
+
14
+ ```
15
+ node dist/cli.js grid \
16
+ --inputs "C:\\path\\to\\img1.png,C:\\path\\to\\img2.png,C:\\path\\to\\img3.png,C:\\path\\to\\img4.png" \
17
+ --output "C:\\path\\to\\grid.png" \
18
+ --columns 2 --rows 2 --background "#000000" --padding 20
19
+ ```
20
+
21
+ ---
22
+
23
+ ## 1) API Location
24
+
25
+ - Helper: `src/image/grid.ts`
26
+ - Export: `createImageGrid`
27
+ - CLI: `grid`
28
+
29
+ ---
30
+
31
+ ## 2) Function Signature
32
+
33
+ ```ts
34
+ export const createImageGrid = async (
35
+ inputPaths: string[],
36
+ outputPath: string,
37
+ options: GridOptions
38
+ ): Promise<void>
39
+ ```
40
+
41
+ ### GridOptions
42
+
43
+ ```ts
44
+ export interface GridOptions {
45
+ columns: number;
46
+ rows: number;
47
+ padding?: number; // default 20
48
+ background?: string;// default #000000
49
+ tileWidth?: number; // optional fixed tile size
50
+ tileHeight?: number;// optional fixed tile size
51
+ }
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 3) Behavior
57
+
58
+ - Creates a blank canvas sized to fit all tiles + padding.
59
+ - Resizes each image to the tile size with `fit: contain` (keeps aspect ratio).
60
+ - Composites images into row/column slots.
61
+ - Unused slots are left as background.
62
+
63
+ ---
64
+
65
+ ## 4) CLI Usage
66
+
67
+ ```
68
+ node dist/cli.js grid --inputs "img1.png,img2.png,img3.png,img4.png" --output "grid.png" --columns 2 --rows 2 --background "#000000" --padding 20
69
+ ```
70
+
71
+ ---
72
+
73
+ ## 5) SDK Example
74
+
75
+ ```ts
76
+ import { createImageGrid } from "./dist/index.js";
77
+
78
+ await createImageGrid(
79
+ ["img1.png", "img2.png", "img3.png", "img4.png"],
80
+ "grid.png",
81
+ { columns: 2, rows: 2, padding: 20, background: "#000000" }
82
+ );
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Related Skills
88
+
89
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\generate-images.md`
90
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\generate-video.md`
91
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\refine-images.md`
92
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\muse-management.md`
93
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\tired-girl.md`
94
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\image-grid.md`
95
+ - `C:\\Users\\karim\\Documents\\local-lumiere\\.agents\\skills\\config-troubleshooting.md`
96
+
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: image-overlay
3
+ description: Overlay one image (logo, watermark, sticker) onto another at a chosen size and position. Use when asked to slap/paste a logo on top of an image, add a watermark, composite two images, or place an image at specific coordinates.
4
+ ---
5
+
6
+ # Image Overlay
7
+
8
+ ## Quick Start
9
+
10
+ ```bash
11
+ node .agents/skills/image-overlay/scripts/overlay-image.js \
12
+ --base path/to/base.jpg \
13
+ --overlay path/to/logo.png \
14
+ --output path/to/output.png \
15
+ --scale 0.2 \
16
+ --position bottom-right \
17
+ --margin 24
18
+ ```
19
+
20
+ ## Inputs
21
+
22
+ - `--base`: path to the background image.
23
+ - `--overlay`: path to the image/logo to place on top.
24
+ - `--output`: path for the final composited image.
25
+
26
+ ## Sizing
27
+
28
+ - Use `--scale` to size the overlay relative to base width. Default is `0.2` if no size is provided.
29
+ - Use `--width` and/or `--height` for explicit pixel sizing.
30
+ - If both `--width` and `--height` are provided, the overlay is resized with `contain` to preserve aspect ratio.
31
+
32
+ Example (explicit width):
33
+
34
+ ```bash
35
+ node .agents/skills/image-overlay/scripts/overlay-image.js \
36
+ --base input.jpg \
37
+ --overlay logo.png \
38
+ --output output.png \
39
+ --width 320 \
40
+ --position top-right
41
+ ```
42
+
43
+ ## Positioning
44
+
45
+ - Use `--position` with one of: `top-left`, `top-center`, `top-right`, `center`, `bottom-left`, `bottom-center`, `bottom-right`.
46
+ - Use `--margin` to offset from edges (default `24`).
47
+ - Use `--offset-x` and `--offset-y` to nudge the overlay relative to the chosen position.
48
+ - Use `--x` and `--y` to place the overlay at absolute coordinates (overrides `--position`).
49
+ - Use `--opacity` between `0` and `1` to make the overlay more subtle (e.g., `0.75`).
50
+
51
+ Example (absolute coordinates):
52
+
53
+ ```bash
54
+ node .agents/skills/image-overlay/scripts/overlay-image.js \
55
+ --base input.jpg \
56
+ --overlay logo.png \
57
+ --output output.png \
58
+ --scale 0.15 \
59
+ --x 120 \
60
+ --y 80
61
+ ```
62
+
63
+ ## Script
64
+
65
+ - `scripts/overlay-image.js` performs the composite using Sharp.
66
+ - The script auto-rotates images via EXIF orientation and clamps the overlay within the base image bounds.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Image Overlay"
3
+ short_description: "Overlay a logo onto another image."
4
+ default_prompt: "Overlay one image onto another at a specified size and position using the script in this skill."
@@ -0,0 +1,218 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import sharp from "sharp";
4
+
5
+ const USAGE = `
6
+ Overlay one image onto another.
7
+
8
+ Usage:
9
+ node .agents/skills/image-overlay/scripts/overlay-image.js \
10
+ --base path/to/base.jpg \
11
+ --overlay path/to/logo.png \
12
+ --output path/to/output.png \
13
+ [--scale 0.2] [--width 320] [--height 200] \
14
+ [--position bottom-right] [--margin 24] \
15
+ [--offset-x 0] [--offset-y 0] \
16
+ [--x 120] [--y 80] \
17
+ [--opacity 0.75]
18
+
19
+ Notes:
20
+ - Provide either --scale or --width/--height.
21
+ - If none are provided, default scale is 0.2 (20% of base width).
22
+ - If --x/--y are provided, they override --position.
23
+ `;
24
+
25
+ function parseArgs(argv) {
26
+ const args = {};
27
+ for (let i = 0; i < argv.length; i += 1) {
28
+ const token = argv[i];
29
+ if (!token.startsWith("--")) continue;
30
+ const key = token.slice(2);
31
+ const next = argv[i + 1];
32
+ if (!next || next.startsWith("--")) {
33
+ args[key] = true;
34
+ continue;
35
+ }
36
+ args[key] = next;
37
+ i += 1;
38
+ }
39
+ return args;
40
+ }
41
+
42
+ function toNumber(value, label) {
43
+ if (value === undefined) return undefined;
44
+ const parsed = Number(value);
45
+ if (!Number.isFinite(parsed)) {
46
+ throw new Error(`Invalid ${label}: ${value}`);
47
+ }
48
+ return parsed;
49
+ }
50
+
51
+ function clamp(value, min, max) {
52
+ return Math.min(Math.max(value, min), max);
53
+ }
54
+
55
+ const args = parseArgs(process.argv.slice(2));
56
+
57
+ if (args.help) {
58
+ console.log(USAGE);
59
+ process.exit(0);
60
+ }
61
+
62
+ const basePath = args.base;
63
+ const overlayPath = args.overlay;
64
+ const outputPath = args.output;
65
+
66
+ if (!basePath || !overlayPath || !outputPath) {
67
+ console.error("Missing required arguments: --base, --overlay, --output");
68
+ console.log(USAGE);
69
+ process.exit(1);
70
+ }
71
+
72
+ const scaleArg = toNumber(args.scale, "scale");
73
+ const opacityArg = toNumber(args.opacity, "opacity");
74
+ const widthArg = toNumber(args.width, "width");
75
+ const heightArg = toNumber(args.height, "height");
76
+
77
+ let scale = scaleArg;
78
+ if (scale === undefined && widthArg === undefined && heightArg === undefined) {
79
+ scale = 0.2;
80
+ }
81
+
82
+ if (scale !== undefined && scale <= 0) {
83
+ throw new Error("Scale must be greater than 0.");
84
+ }
85
+ if (widthArg !== undefined && widthArg <= 0) {
86
+ throw new Error("Width must be greater than 0.");
87
+ }
88
+ if (heightArg !== undefined && heightArg <= 0) {
89
+ throw new Error("Height must be greater than 0.");
90
+ }
91
+ if (opacityArg !== undefined && (opacityArg < 0 || opacityArg > 1)) {
92
+ throw new Error("Opacity must be between 0 and 1.");
93
+ }
94
+
95
+ const baseImage = sharp(basePath).rotate();
96
+ const baseMeta = await baseImage.metadata();
97
+
98
+ if (!baseMeta.width || !baseMeta.height) {
99
+ throw new Error("Unable to read base image dimensions.");
100
+ }
101
+
102
+ const resizeOptions = {};
103
+ if (scale !== undefined) {
104
+ resizeOptions.width = Math.max(1, Math.round(baseMeta.width * scale));
105
+ } else {
106
+ if (widthArg !== undefined) resizeOptions.width = Math.round(widthArg);
107
+ if (heightArg !== undefined) resizeOptions.height = Math.round(heightArg);
108
+ }
109
+ if (resizeOptions.width || resizeOptions.height) {
110
+ resizeOptions.fit = "contain";
111
+ }
112
+
113
+ let overlaySharp = sharp(overlayPath).rotate();
114
+ if (resizeOptions.width || resizeOptions.height) {
115
+ overlaySharp = overlaySharp.resize(resizeOptions);
116
+ }
117
+ let overlayBuffer;
118
+ let overlayWidth;
119
+ let overlayHeight;
120
+
121
+ if (opacityArg !== undefined && opacityArg < 1) {
122
+ const { data, info } = await overlaySharp
123
+ .ensureAlpha()
124
+ .raw()
125
+ .toBuffer({ resolveWithObject: true });
126
+
127
+ for (let i = 3; i < data.length; i += 4) {
128
+ data[i] = Math.round(data[i] * opacityArg);
129
+ }
130
+
131
+ overlayWidth = info.width;
132
+ overlayHeight = info.height;
133
+ overlayBuffer = await sharp(data, {
134
+ raw: {
135
+ width: info.width,
136
+ height: info.height,
137
+ channels: 4
138
+ }
139
+ })
140
+ .png()
141
+ .toBuffer();
142
+ } else {
143
+ const { data, info } = await overlaySharp.png().toBuffer({ resolveWithObject: true });
144
+ overlayWidth = info.width;
145
+ overlayHeight = info.height;
146
+ overlayBuffer = data;
147
+ }
148
+
149
+ if (!overlayWidth || !overlayHeight) {
150
+ throw new Error("Unable to read overlay image dimensions.");
151
+ }
152
+
153
+ const margin = Math.round(toNumber(args.margin, "margin") ?? 24);
154
+ const offsetX = Math.round(toNumber(args["offset-x"], "offset-x") ?? 0);
155
+ const offsetY = Math.round(toNumber(args["offset-y"], "offset-y") ?? 0);
156
+
157
+ let left;
158
+ let top;
159
+
160
+ const xArg = toNumber(args.x, "x");
161
+ const yArg = toNumber(args.y, "y");
162
+
163
+ if (xArg !== undefined || yArg !== undefined) {
164
+ left = Math.round(xArg ?? margin);
165
+ top = Math.round(yArg ?? margin);
166
+ } else {
167
+ const position = String(args.position ?? "bottom-right").toLowerCase();
168
+ switch (position) {
169
+ case "top-left":
170
+ left = margin;
171
+ top = margin;
172
+ break;
173
+ case "top-center":
174
+ left = (baseMeta.width - overlayWidth) / 2;
175
+ top = margin;
176
+ break;
177
+ case "top-right":
178
+ left = baseMeta.width - overlayWidth - margin;
179
+ top = margin;
180
+ break;
181
+ case "center":
182
+ left = (baseMeta.width - overlayWidth) / 2;
183
+ top = (baseMeta.height - overlayHeight) / 2;
184
+ break;
185
+ case "bottom-left":
186
+ left = margin;
187
+ top = baseMeta.height - overlayHeight - margin;
188
+ break;
189
+ case "bottom-center":
190
+ left = (baseMeta.width - overlayWidth) / 2;
191
+ top = baseMeta.height - overlayHeight - margin;
192
+ break;
193
+ case "bottom-right":
194
+ default:
195
+ left = baseMeta.width - overlayWidth - margin;
196
+ top = baseMeta.height - overlayHeight - margin;
197
+ break;
198
+ }
199
+ }
200
+
201
+ left = clamp(Math.round(left + offsetX), 0, Math.max(0, baseMeta.width - overlayWidth));
202
+ top = clamp(Math.round(top + offsetY), 0, Math.max(0, baseMeta.height - overlayHeight));
203
+
204
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
205
+
206
+ await baseImage
207
+ .composite([
208
+ {
209
+ input: overlayBuffer,
210
+ left,
211
+ top
212
+ }
213
+ ])
214
+ .toFile(outputPath);
215
+
216
+ console.log(`Overlay complete: ${outputPath}`);
217
+ console.log(`Overlay size: ${overlayWidth}x${overlayHeight}`);
218
+ console.log(`Position: left=${left}, top=${top}`);