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.
- package/.agents/skills/annotate-image/SKILL.md +99 -0
- package/.agents/skills/config-troubleshooting/SKILL.md +97 -0
- package/.agents/skills/generate-images/SKILL.md +667 -0
- package/.agents/skills/generate-video/SKILL.md +328 -0
- package/.agents/skills/image-grid/SKILL.md +96 -0
- package/.agents/skills/image-overlay/SKILL.md +66 -0
- package/.agents/skills/image-overlay/agents/openai.yaml +4 -0
- package/.agents/skills/image-overlay/scripts/overlay-image.js +218 -0
- package/.agents/skills/muse-management/SKILL.md +232 -0
- package/.agents/skills/refine-images/SKILL.md +192 -0
- package/.agents/skills/tired-girl/SKILL.md +131 -0
- package/.env.example +2 -0
- package/AGENTS.md +66 -0
- package/README.md +96 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +214 -0
- package/dist/cli.js.map +1 -0
- package/dist/clients/geminiClient.d.ts +37 -0
- package/dist/clients/geminiClient.js +129 -0
- package/dist/clients/geminiClient.js.map +1 -0
- package/dist/config/constants.d.ts +63 -0
- package/dist/config/constants.js +1005 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/options.d.ts +1 -0
- package/dist/config/options.js +2 -0
- package/dist/config/options.js.map +1 -0
- package/dist/config/templates.d.ts +3 -0
- package/dist/config/templates.js +4 -0
- package/dist/config/templates.js.map +1 -0
- package/dist/config/tiredGirl.d.ts +3 -0
- package/dist/config/tiredGirl.js +9 -0
- package/dist/config/tiredGirl.js.map +1 -0
- package/dist/image/annotate.d.ts +2 -0
- package/dist/image/annotate.js +119 -0
- package/dist/image/annotate.js.map +1 -0
- package/dist/image/grid.d.ts +2 -0
- package/dist/image/grid.js +44 -0
- package/dist/image/grid.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/pipelines/createMuse.d.ts +3 -0
- package/dist/pipelines/createMuse.js +49 -0
- package/dist/pipelines/createMuse.js.map +1 -0
- package/dist/pipelines/generateImages.d.ts +2 -0
- package/dist/pipelines/generateImages.js +140 -0
- package/dist/pipelines/generateImages.js.map +1 -0
- package/dist/pipelines/generateTiredGirl.d.ts +2 -0
- package/dist/pipelines/generateTiredGirl.js +73 -0
- package/dist/pipelines/generateTiredGirl.js.map +1 -0
- package/dist/pipelines/generateVideo.d.ts +2 -0
- package/dist/pipelines/generateVideo.js +27 -0
- package/dist/pipelines/generateVideo.js.map +1 -0
- package/dist/pipelines/refineImage.d.ts +2 -0
- package/dist/pipelines/refineImage.js +28 -0
- package/dist/pipelines/refineImage.js.map +1 -0
- package/dist/pipelines/resolve.d.ts +11 -0
- package/dist/pipelines/resolve.js +74 -0
- package/dist/pipelines/resolve.js.map +1 -0
- package/dist/pipelines/upscaleImage.d.ts +2 -0
- package/dist/pipelines/upscaleImage.js +23 -0
- package/dist/pipelines/upscaleImage.js.map +1 -0
- package/dist/prompt/buildPrompt.d.ts +4 -0
- package/dist/prompt/buildPrompt.js +322 -0
- package/dist/prompt/buildPrompt.js.map +1 -0
- package/dist/prompt/tiredGirlPrompt.d.ts +3 -0
- package/dist/prompt/tiredGirlPrompt.js +33 -0
- package/dist/prompt/tiredGirlPrompt.js.map +1 -0
- package/dist/storage/files.d.ts +15 -0
- package/dist/storage/files.js +34 -0
- package/dist/storage/files.js.map +1 -0
- package/dist/storage/museStore.d.ts +5 -0
- package/dist/storage/museStore.js +26 -0
- package/dist/storage/museStore.js.map +1 -0
- package/dist/types.d.ts +169 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/examples/generate.d.ts +1 -0
- package/examples/generate.js +28 -0
- package/examples/generate.js.map +1 -0
- package/examples/generate.ts +30 -0
- package/examples/muse.d.ts +1 -0
- package/examples/muse.js +18 -0
- package/examples/muse.js.map +1 -0
- package/examples/muse.ts +20 -0
- package/examples/video.d.ts +1 -0
- package/examples/video.js +18 -0
- package/examples/video.js.map +1 -0
- package/examples/video.ts +20 -0
- package/logo-round.png +0 -0
- package/logo.jpeg +0 -0
- package/package.json +27 -0
- package/src/cli.ts +259 -0
- package/src/clients/geminiClient.ts +168 -0
- package/src/config/constants.ts +1105 -0
- package/src/config/options.ts +15 -0
- package/src/config/templates.ts +4 -0
- package/src/config/tiredGirl.ts +11 -0
- package/src/image/annotate.ts +139 -0
- package/src/image/grid.ts +58 -0
- package/src/index.ts +27 -0
- package/src/pipelines/createMuse.ts +76 -0
- package/src/pipelines/generateImages.ts +203 -0
- package/src/pipelines/generateTiredGirl.ts +86 -0
- package/src/pipelines/generateVideo.ts +36 -0
- package/src/pipelines/refineImage.ts +36 -0
- package/src/pipelines/resolve.ts +88 -0
- package/src/pipelines/upscaleImage.ts +30 -0
- package/src/prompt/buildPrompt.ts +380 -0
- package/src/prompt/tiredGirlPrompt.ts +35 -0
- package/src/storage/files.ts +41 -0
- package/src/storage/museStore.ts +31 -0
- package/src/types.ts +198 -0
- 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,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}`);
|