gunni 0.3.0 → 0.3.3
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/index.js +509 -1232
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,276 +9,6 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/core/model-registry.ts
|
|
13
|
-
var model_registry_exports = {};
|
|
14
|
-
__export(model_registry_exports, {
|
|
15
|
-
DEFAULTS: () => DEFAULTS,
|
|
16
|
-
getAllModels: () => getAllModels,
|
|
17
|
-
getCategories: () => getCategories,
|
|
18
|
-
getDefaultModel: () => getDefaultModel,
|
|
19
|
-
getModel: () => getModel,
|
|
20
|
-
getModelsByCategory: () => getModelsByCategory
|
|
21
|
-
});
|
|
22
|
-
function getAllModels() {
|
|
23
|
-
return MODELS;
|
|
24
|
-
}
|
|
25
|
-
function getModelsByCategory(category) {
|
|
26
|
-
return MODELS.filter((m) => m.category === category);
|
|
27
|
-
}
|
|
28
|
-
function getModel(id) {
|
|
29
|
-
return MODELS.find((m) => m.id === id);
|
|
30
|
-
}
|
|
31
|
-
function getDefaultModel(category) {
|
|
32
|
-
return MODELS.find((m) => m.category === category && m.isDefault);
|
|
33
|
-
}
|
|
34
|
-
function getCategories() {
|
|
35
|
-
return [...new Set(MODELS.map((m) => m.category))];
|
|
36
|
-
}
|
|
37
|
-
var DEFAULTS, MODELS;
|
|
38
|
-
var init_model_registry = __esm({
|
|
39
|
-
"src/core/model-registry.ts"() {
|
|
40
|
-
"use strict";
|
|
41
|
-
DEFAULTS = {
|
|
42
|
-
image: "nano-banana",
|
|
43
|
-
edit: "nano-banana-edit",
|
|
44
|
-
describe: "florence-2",
|
|
45
|
-
upscale: "topaz-upscale",
|
|
46
|
-
removeBg: "bria-bg-remove",
|
|
47
|
-
video: "kling-v3-pro",
|
|
48
|
-
audio: "minimax-speech",
|
|
49
|
-
lipsync: "kling-lipsync",
|
|
50
|
-
design: "recraft-v4"
|
|
51
|
-
};
|
|
52
|
-
MODELS = [
|
|
53
|
-
// --- Image generation (hero: nano-banana) ---
|
|
54
|
-
{
|
|
55
|
-
id: "nano-banana",
|
|
56
|
-
name: "Nano Banana Pro",
|
|
57
|
-
provider: "google",
|
|
58
|
-
endpoint: "gemini-3-pro-image-preview",
|
|
59
|
-
category: "image",
|
|
60
|
-
description: "Google's state-of-the-art. Best overall quality and speed. Up to 4K resolution.",
|
|
61
|
-
isDefault: true
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
id: "recraft-v4",
|
|
65
|
-
name: "Recraft V4",
|
|
66
|
-
provider: "fal",
|
|
67
|
-
endpoint: "fal-ai/recraft/v4/text-to-image",
|
|
68
|
-
category: "image",
|
|
69
|
-
description: "Design and marketing. Only model that reliably renders text in images."
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
id: "flux-2-pro",
|
|
73
|
-
name: "Flux 2 Pro",
|
|
74
|
-
provider: "fal",
|
|
75
|
-
endpoint: "fal-ai/flux-2-pro",
|
|
76
|
-
category: "image",
|
|
77
|
-
description: "High-quality photorealism from Black Forest Labs."
|
|
78
|
-
},
|
|
79
|
-
// --- Image editing (hero: nano-banana-edit) ---
|
|
80
|
-
{
|
|
81
|
-
id: "nano-banana-edit",
|
|
82
|
-
name: "Nano Banana Pro Edit",
|
|
83
|
-
provider: "google",
|
|
84
|
-
endpoint: "gemini-3-pro-image-preview",
|
|
85
|
-
category: "edit",
|
|
86
|
-
description: "Google's model for precise image editing. Consistent with generation pipeline.",
|
|
87
|
-
isDefault: true,
|
|
88
|
-
multiImage: true
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
id: "flux-kontext",
|
|
92
|
-
name: "Flux Kontext Pro",
|
|
93
|
-
provider: "fal",
|
|
94
|
-
endpoint: "fal-ai/flux-pro/kontext/max/multi",
|
|
95
|
-
category: "edit",
|
|
96
|
-
description: "Targeted local edits with text and reference images. Supports multiple input images.",
|
|
97
|
-
multiImage: true
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
id: "flux-2-pro-edit",
|
|
101
|
-
name: "Flux 2 Pro Edit",
|
|
102
|
-
provider: "fal",
|
|
103
|
-
endpoint: "fal-ai/flux-2-pro/edit",
|
|
104
|
-
category: "edit",
|
|
105
|
-
description: "Multi-image editing. Combine up to 9 reference images with natural language. Use @image1, @image2 notation in prompts.",
|
|
106
|
-
multiImage: true
|
|
107
|
-
},
|
|
108
|
-
// --- Upscale (hero: topaz) ---
|
|
109
|
-
{
|
|
110
|
-
id: "topaz-upscale",
|
|
111
|
-
name: "Topaz Upscale",
|
|
112
|
-
provider: "fal",
|
|
113
|
-
endpoint: "fal-ai/topaz/upscale/image",
|
|
114
|
-
category: "upscale",
|
|
115
|
-
description: "Industry-standard image upscaling and enhancement.",
|
|
116
|
-
isDefault: true
|
|
117
|
-
},
|
|
118
|
-
// --- Video (hero: kling-v3-pro) ---
|
|
119
|
-
{
|
|
120
|
-
id: "kling-v3-pro",
|
|
121
|
-
name: "Kling V3 Pro",
|
|
122
|
-
provider: "fal",
|
|
123
|
-
endpoint: "fal-ai/kling-video/v3/pro/image-to-video",
|
|
124
|
-
category: "video",
|
|
125
|
-
description: "Cinematic video with fluid motion and native audio.",
|
|
126
|
-
isDefault: true
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
id: "kling-v3-pro-t2v",
|
|
130
|
-
name: "Kling V3 Pro (Text)",
|
|
131
|
-
provider: "fal",
|
|
132
|
-
endpoint: "fal-ai/kling-video/v3/pro/text-to-video",
|
|
133
|
-
category: "video",
|
|
134
|
-
description: "Kling V3 Pro text-to-video. No source image needed."
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
id: "veo-3.1",
|
|
138
|
-
name: "Veo 3.1",
|
|
139
|
-
provider: "fal",
|
|
140
|
-
endpoint: "fal-ai/veo3.1/image-to-video",
|
|
141
|
-
category: "video",
|
|
142
|
-
description: "Google's video generation with sound, via Fal."
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
id: "veo-3.1-t2v",
|
|
146
|
-
name: "Veo 3.1 (Text)",
|
|
147
|
-
provider: "fal",
|
|
148
|
-
endpoint: "fal-ai/veo3.1",
|
|
149
|
-
category: "video",
|
|
150
|
-
description: "Veo 3.1 text-to-video with native audio."
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
id: "veo-3.1-fast",
|
|
154
|
-
name: "Veo 3.1 Fast",
|
|
155
|
-
provider: "fal",
|
|
156
|
-
endpoint: "fal-ai/veo3.1/fast/image-to-video",
|
|
157
|
-
category: "video",
|
|
158
|
-
description: "Veo 3.1 Fast \u2014 62% cheaper, slightly lower quality. Image-to-video with sound."
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
id: "veo-3.1-fast-t2v",
|
|
162
|
-
name: "Veo 3.1 Fast (Text)",
|
|
163
|
-
provider: "fal",
|
|
164
|
-
endpoint: "fal-ai/veo3.1/fast",
|
|
165
|
-
category: "video",
|
|
166
|
-
description: "Veo 3.1 Fast text-to-video with native audio. Budget-friendly option."
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
id: "minimax-i2v",
|
|
170
|
-
name: "MiniMax Hailuo 2.3",
|
|
171
|
-
provider: "fal",
|
|
172
|
-
endpoint: "fal-ai/minimax/hailuo-2.3/pro/image-to-video",
|
|
173
|
-
category: "video",
|
|
174
|
-
description: "MiniMax Hailuo 2.3 Pro image-to-video. 1080p output."
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
id: "wan-2.6",
|
|
178
|
-
name: "Wan 2.6",
|
|
179
|
-
provider: "fal",
|
|
180
|
-
endpoint: "wan/v2.6/image-to-video",
|
|
181
|
-
category: "video",
|
|
182
|
-
description: "Alibaba Wan 2.6 image-to-video. Up to 1080p with native audio."
|
|
183
|
-
},
|
|
184
|
-
// --- Audio (hero: minimax-speech) ---
|
|
185
|
-
{
|
|
186
|
-
id: "minimax-speech",
|
|
187
|
-
name: "MiniMax Speech 2.8 HD",
|
|
188
|
-
provider: "fal",
|
|
189
|
-
endpoint: "fal-ai/minimax/speech-2.8-hd",
|
|
190
|
-
category: "audio",
|
|
191
|
-
description: "High-quality text-to-speech with multiple voices.",
|
|
192
|
-
isDefault: true
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
id: "elevenlabs-tts",
|
|
196
|
-
name: "ElevenLabs TTS v3",
|
|
197
|
-
provider: "fal",
|
|
198
|
-
endpoint: "fal-ai/elevenlabs/tts/turbo-v2.5",
|
|
199
|
-
category: "audio",
|
|
200
|
-
description: "Natural, conversational text-to-speech. Best for UGC voiceovers and narration."
|
|
201
|
-
},
|
|
202
|
-
// --- Lip sync (hero: kling-lipsync) ---
|
|
203
|
-
{
|
|
204
|
-
id: "kling-lipsync",
|
|
205
|
-
name: "Kling LipSync",
|
|
206
|
-
provider: "fal",
|
|
207
|
-
endpoint: "fal-ai/kling-video/lipsync/audio-to-video",
|
|
208
|
-
category: "lipsync",
|
|
209
|
-
description: "Lip-sync audio onto a video. Input: source video + audio track. Best quality lip sync.",
|
|
210
|
-
isDefault: true
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
id: "kling-avatar",
|
|
214
|
-
name: "Kling AI Avatar v2",
|
|
215
|
-
provider: "fal",
|
|
216
|
-
endpoint: "fal-ai/kling-video/ai-avatar/v2/standard",
|
|
217
|
-
category: "lipsync",
|
|
218
|
-
description: "Generate talking head video from a still image + audio. One-step avatar animation."
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
id: "sync-lipsync",
|
|
222
|
-
name: "Sync Labs Lipsync 2.0",
|
|
223
|
-
provider: "fal",
|
|
224
|
-
endpoint: "fal-ai/sync-lipsync",
|
|
225
|
-
category: "lipsync",
|
|
226
|
-
description: "Advanced lip sync with natural motion. Alternative to Kling LipSync."
|
|
227
|
-
},
|
|
228
|
-
// --- Describe / vision (hero: florence-2) ---
|
|
229
|
-
{
|
|
230
|
-
id: "florence-2",
|
|
231
|
-
name: "Florence 2 Large",
|
|
232
|
-
provider: "fal",
|
|
233
|
-
endpoint: "fal-ai/florence-2-large/detailed-caption",
|
|
234
|
-
category: "describe",
|
|
235
|
-
description: "Detailed image captioning and description.",
|
|
236
|
-
isDefault: true
|
|
237
|
-
},
|
|
238
|
-
// --- OpenAI image generation ---
|
|
239
|
-
{
|
|
240
|
-
id: "gpt-image",
|
|
241
|
-
name: "GPT Image 1.5",
|
|
242
|
-
provider: "openai",
|
|
243
|
-
endpoint: "gpt-image-1.5",
|
|
244
|
-
category: "image",
|
|
245
|
-
description: "OpenAI's latest image model. Best-in-class text rendering and editing.",
|
|
246
|
-
isDefault: false
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
id: "gpt-image-mini",
|
|
250
|
-
name: "GPT Image Mini",
|
|
251
|
-
provider: "openai",
|
|
252
|
-
endpoint: "gpt-image-1-mini",
|
|
253
|
-
category: "image",
|
|
254
|
-
description: "Cost-optimized OpenAI image model. Great for high-volume generation.",
|
|
255
|
-
isDefault: false
|
|
256
|
-
},
|
|
257
|
-
// --- OpenAI image editing ---
|
|
258
|
-
{
|
|
259
|
-
id: "gpt-image-edit",
|
|
260
|
-
name: "GPT Image 1.5 Edit",
|
|
261
|
-
provider: "openai",
|
|
262
|
-
endpoint: "gpt-image-1.5",
|
|
263
|
-
category: "edit",
|
|
264
|
-
description: "OpenAI image editing. Surgical precision for inpainting and modifications.",
|
|
265
|
-
isDefault: false,
|
|
266
|
-
multiImage: true
|
|
267
|
-
},
|
|
268
|
-
// --- Utility ---
|
|
269
|
-
{
|
|
270
|
-
id: "bria-bg-remove",
|
|
271
|
-
name: "Bria Background Removal",
|
|
272
|
-
provider: "fal",
|
|
273
|
-
endpoint: "fal-ai/bria/background/remove",
|
|
274
|
-
category: "utility",
|
|
275
|
-
description: "Remove backgrounds from images.",
|
|
276
|
-
isDefault: true
|
|
277
|
-
}
|
|
278
|
-
];
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
|
|
282
12
|
// src/core/supabase.ts
|
|
283
13
|
import { createClient } from "@supabase/supabase-js";
|
|
284
14
|
function getSupabase() {
|
|
@@ -422,7 +152,7 @@ var init_refs = __esm({
|
|
|
422
152
|
|
|
423
153
|
// src/index.ts
|
|
424
154
|
import { Command } from "commander";
|
|
425
|
-
import
|
|
155
|
+
import pc21 from "picocolors";
|
|
426
156
|
|
|
427
157
|
// src/commands/image.ts
|
|
428
158
|
import { existsSync } from "fs";
|
|
@@ -573,8 +303,253 @@ async function detectImageFormat(filePath) {
|
|
|
573
303
|
return void 0;
|
|
574
304
|
}
|
|
575
305
|
|
|
576
|
-
//
|
|
577
|
-
|
|
306
|
+
// data/models.json
|
|
307
|
+
var models_default = {
|
|
308
|
+
defaults: {
|
|
309
|
+
image: "nano-banana",
|
|
310
|
+
edit: "nano-banana-edit",
|
|
311
|
+
describe: "florence-2",
|
|
312
|
+
upscale: "topaz-upscale",
|
|
313
|
+
removeBg: "bria-bg-remove",
|
|
314
|
+
video: "kling-v3-pro",
|
|
315
|
+
audio: "minimax-speech",
|
|
316
|
+
lipsync: "kling-lipsync",
|
|
317
|
+
design: "recraft-v4"
|
|
318
|
+
},
|
|
319
|
+
models: [
|
|
320
|
+
{
|
|
321
|
+
id: "nano-banana",
|
|
322
|
+
name: "Nano Banana Pro",
|
|
323
|
+
provider: "google",
|
|
324
|
+
endpoint: "gemini-3-pro-image-preview",
|
|
325
|
+
category: "image",
|
|
326
|
+
description: "Google's state-of-the-art. Best overall quality and speed. Up to 4K resolution.",
|
|
327
|
+
isDefault: true
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: "recraft-v4",
|
|
331
|
+
name: "Recraft V4",
|
|
332
|
+
provider: "fal",
|
|
333
|
+
endpoint: "fal-ai/recraft/v4/text-to-image",
|
|
334
|
+
category: "image",
|
|
335
|
+
description: "Design and marketing. Only model that reliably renders text in images."
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: "flux-2-pro",
|
|
339
|
+
name: "Flux 2 Pro",
|
|
340
|
+
provider: "fal",
|
|
341
|
+
endpoint: "fal-ai/flux-2-pro",
|
|
342
|
+
category: "image",
|
|
343
|
+
description: "High-quality photorealism from Black Forest Labs."
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: "nano-banana-edit",
|
|
347
|
+
name: "Nano Banana Pro Edit",
|
|
348
|
+
provider: "google",
|
|
349
|
+
endpoint: "gemini-3-pro-image-preview",
|
|
350
|
+
category: "edit",
|
|
351
|
+
description: "Google's model for precise image editing. Consistent with generation pipeline.",
|
|
352
|
+
isDefault: true,
|
|
353
|
+
multiImage: true
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: "flux-kontext",
|
|
357
|
+
name: "Flux Kontext Pro",
|
|
358
|
+
provider: "fal",
|
|
359
|
+
endpoint: "fal-ai/flux-pro/kontext/max/multi",
|
|
360
|
+
category: "edit",
|
|
361
|
+
description: "Targeted local edits with text and reference images. Supports multiple input images.",
|
|
362
|
+
multiImage: true
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: "flux-2-pro-edit",
|
|
366
|
+
name: "Flux 2 Pro Edit",
|
|
367
|
+
provider: "fal",
|
|
368
|
+
endpoint: "fal-ai/flux-2-pro/edit",
|
|
369
|
+
category: "edit",
|
|
370
|
+
description: "Multi-image editing. Combine up to 9 reference images with natural language. Use @image1, @image2 notation in prompts.",
|
|
371
|
+
multiImage: true
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
id: "topaz-upscale",
|
|
375
|
+
name: "Topaz Upscale",
|
|
376
|
+
provider: "fal",
|
|
377
|
+
endpoint: "fal-ai/topaz/upscale/image",
|
|
378
|
+
category: "upscale",
|
|
379
|
+
description: "Industry-standard image upscaling and enhancement.",
|
|
380
|
+
isDefault: true
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: "kling-v3-pro",
|
|
384
|
+
name: "Kling V3 Pro",
|
|
385
|
+
provider: "fal",
|
|
386
|
+
endpoint: "fal-ai/kling-video/v3/pro/image-to-video",
|
|
387
|
+
category: "video",
|
|
388
|
+
description: "Cinematic video with fluid motion and native audio.",
|
|
389
|
+
isDefault: true
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
id: "kling-v3-pro-t2v",
|
|
393
|
+
name: "Kling V3 Pro (Text)",
|
|
394
|
+
provider: "fal",
|
|
395
|
+
endpoint: "fal-ai/kling-video/v3/pro/text-to-video",
|
|
396
|
+
category: "video",
|
|
397
|
+
description: "Kling V3 Pro text-to-video. No source image needed."
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
id: "veo-3.1",
|
|
401
|
+
name: "Veo 3.1",
|
|
402
|
+
provider: "fal",
|
|
403
|
+
endpoint: "fal-ai/veo3.1/image-to-video",
|
|
404
|
+
category: "video",
|
|
405
|
+
description: "Google's video generation with sound, via Fal."
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
id: "veo-3.1-t2v",
|
|
409
|
+
name: "Veo 3.1 (Text)",
|
|
410
|
+
provider: "fal",
|
|
411
|
+
endpoint: "fal-ai/veo3.1",
|
|
412
|
+
category: "video",
|
|
413
|
+
description: "Veo 3.1 text-to-video with native audio."
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: "veo-3.1-fast",
|
|
417
|
+
name: "Veo 3.1 Fast",
|
|
418
|
+
provider: "fal",
|
|
419
|
+
endpoint: "fal-ai/veo3.1/fast/image-to-video",
|
|
420
|
+
category: "video",
|
|
421
|
+
description: "Veo 3.1 Fast \u2014 62% cheaper, slightly lower quality. Image-to-video with sound."
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: "veo-3.1-fast-t2v",
|
|
425
|
+
name: "Veo 3.1 Fast (Text)",
|
|
426
|
+
provider: "fal",
|
|
427
|
+
endpoint: "fal-ai/veo3.1/fast",
|
|
428
|
+
category: "video",
|
|
429
|
+
description: "Veo 3.1 Fast text-to-video with native audio. Budget-friendly option."
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
id: "minimax-i2v",
|
|
433
|
+
name: "MiniMax Hailuo 2.3",
|
|
434
|
+
provider: "fal",
|
|
435
|
+
endpoint: "fal-ai/minimax/hailuo-2.3/pro/image-to-video",
|
|
436
|
+
category: "video",
|
|
437
|
+
description: "MiniMax Hailuo 2.3 Pro image-to-video. 1080p output."
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: "wan-2.6",
|
|
441
|
+
name: "Wan 2.6",
|
|
442
|
+
provider: "fal",
|
|
443
|
+
endpoint: "wan/v2.6/image-to-video",
|
|
444
|
+
category: "video",
|
|
445
|
+
description: "Alibaba Wan 2.6 image-to-video. Up to 1080p with native audio."
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
id: "minimax-speech",
|
|
449
|
+
name: "MiniMax Speech 2.8 HD",
|
|
450
|
+
provider: "fal",
|
|
451
|
+
endpoint: "fal-ai/minimax/speech-2.8-hd",
|
|
452
|
+
category: "audio",
|
|
453
|
+
description: "High-quality text-to-speech with multiple voices.",
|
|
454
|
+
isDefault: true
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
id: "elevenlabs-tts",
|
|
458
|
+
name: "ElevenLabs TTS v3",
|
|
459
|
+
provider: "fal",
|
|
460
|
+
endpoint: "fal-ai/elevenlabs/tts/turbo-v2.5",
|
|
461
|
+
category: "audio",
|
|
462
|
+
description: "Natural, conversational text-to-speech. Best for UGC voiceovers and narration."
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
id: "kling-lipsync",
|
|
466
|
+
name: "Kling LipSync",
|
|
467
|
+
provider: "fal",
|
|
468
|
+
endpoint: "fal-ai/kling-video/lipsync/audio-to-video",
|
|
469
|
+
category: "lipsync",
|
|
470
|
+
description: "Lip-sync audio onto a video. Input: source video + audio track. Best quality lip sync.",
|
|
471
|
+
isDefault: true
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
id: "kling-avatar",
|
|
475
|
+
name: "Kling AI Avatar v2",
|
|
476
|
+
provider: "fal",
|
|
477
|
+
endpoint: "fal-ai/kling-video/ai-avatar/v2/standard",
|
|
478
|
+
category: "lipsync",
|
|
479
|
+
description: "Generate talking head video from a still image + audio. One-step avatar animation."
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: "sync-lipsync",
|
|
483
|
+
name: "Sync Labs Lipsync 2.0",
|
|
484
|
+
provider: "fal",
|
|
485
|
+
endpoint: "fal-ai/sync-lipsync",
|
|
486
|
+
category: "lipsync",
|
|
487
|
+
description: "Advanced lip sync with natural motion. Alternative to Kling LipSync."
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
id: "florence-2",
|
|
491
|
+
name: "Florence 2 Large",
|
|
492
|
+
provider: "fal",
|
|
493
|
+
endpoint: "fal-ai/florence-2-large/detailed-caption",
|
|
494
|
+
category: "describe",
|
|
495
|
+
description: "Detailed image captioning and description.",
|
|
496
|
+
isDefault: true
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
id: "gpt-image",
|
|
500
|
+
name: "GPT Image 1.5",
|
|
501
|
+
provider: "openai",
|
|
502
|
+
endpoint: "gpt-image-1.5",
|
|
503
|
+
category: "image",
|
|
504
|
+
description: "OpenAI's latest image model. Best-in-class text rendering and editing.",
|
|
505
|
+
isDefault: false
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: "gpt-image-mini",
|
|
509
|
+
name: "GPT Image Mini",
|
|
510
|
+
provider: "openai",
|
|
511
|
+
endpoint: "gpt-image-1-mini",
|
|
512
|
+
category: "image",
|
|
513
|
+
description: "Cost-optimized OpenAI image model. Great for high-volume generation.",
|
|
514
|
+
isDefault: false
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
id: "gpt-image-edit",
|
|
518
|
+
name: "GPT Image 1.5 Edit",
|
|
519
|
+
provider: "openai",
|
|
520
|
+
endpoint: "gpt-image-1.5",
|
|
521
|
+
category: "edit",
|
|
522
|
+
description: "OpenAI image editing. Surgical precision for inpainting and modifications.",
|
|
523
|
+
isDefault: false,
|
|
524
|
+
multiImage: true
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
id: "bria-bg-remove",
|
|
528
|
+
name: "Bria Background Removal",
|
|
529
|
+
provider: "fal",
|
|
530
|
+
endpoint: "fal-ai/bria/background/remove",
|
|
531
|
+
category: "utility",
|
|
532
|
+
description: "Remove backgrounds from images.",
|
|
533
|
+
isDefault: true
|
|
534
|
+
}
|
|
535
|
+
]
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// src/core/model-registry.ts
|
|
539
|
+
var DEFAULTS = models_default.defaults;
|
|
540
|
+
var MODELS = models_default.models;
|
|
541
|
+
function getAllModels() {
|
|
542
|
+
return MODELS;
|
|
543
|
+
}
|
|
544
|
+
function getModelsByCategory(category) {
|
|
545
|
+
return MODELS.filter((m) => m.category === category);
|
|
546
|
+
}
|
|
547
|
+
function getModel(id) {
|
|
548
|
+
return MODELS.find((m) => m.id === id);
|
|
549
|
+
}
|
|
550
|
+
function getCategories() {
|
|
551
|
+
return [...new Set(MODELS.map((m) => m.category))];
|
|
552
|
+
}
|
|
578
553
|
|
|
579
554
|
// src/providers/base.ts
|
|
580
555
|
function resolveImagePaths(options) {
|
|
@@ -665,9 +640,6 @@ function isNodeError(err) {
|
|
|
665
640
|
return err instanceof Error && "code" in err;
|
|
666
641
|
}
|
|
667
642
|
|
|
668
|
-
// src/providers/fal.ts
|
|
669
|
-
init_model_registry();
|
|
670
|
-
|
|
671
643
|
// src/core/uploader.ts
|
|
672
644
|
import { readFile as readFile3 } from "fs/promises";
|
|
673
645
|
import { basename } from "path";
|
|
@@ -758,9 +730,9 @@ var FalProvider = class extends BaseProvider {
|
|
|
758
730
|
const message = err instanceof Error ? err.message : String(err);
|
|
759
731
|
if (message.includes("Forbidden") || message.includes("401") || message.includes("403")) {
|
|
760
732
|
throw new GunniError(
|
|
761
|
-
"
|
|
733
|
+
"Authentication failed",
|
|
762
734
|
"AUTH_ERROR",
|
|
763
|
-
"Check your key
|
|
735
|
+
"Check your API key and re-run `gunni config`."
|
|
764
736
|
);
|
|
765
737
|
}
|
|
766
738
|
throw new GunniError("Request failed", "PROVIDER_ERROR", message);
|
|
@@ -886,6 +858,28 @@ var FalProvider = class extends BaseProvider {
|
|
|
886
858
|
requestId: result.requestId
|
|
887
859
|
};
|
|
888
860
|
}
|
|
861
|
+
async submitToQueue(endpoint, input) {
|
|
862
|
+
const apiKey = this.overrideApiKey ?? await this.configManager.getApiKey("fal");
|
|
863
|
+
if (!apiKey) {
|
|
864
|
+
throw new GunniError("Not configured yet", "NO_API_KEY", "Run `gunni config` to get started.");
|
|
865
|
+
}
|
|
866
|
+
const { createFalClient } = await import("@fal-ai/client");
|
|
867
|
+
const client2 = createFalClient({ credentials: apiKey });
|
|
868
|
+
const result = await client2.queue.submit(endpoint, { input });
|
|
869
|
+
return result.request_id;
|
|
870
|
+
}
|
|
871
|
+
async checkQueueStatus(endpoint, requestId) {
|
|
872
|
+
const apiKey = this.overrideApiKey ?? await this.configManager.getApiKey("fal");
|
|
873
|
+
if (!apiKey) throw new GunniError("Not configured yet", "NO_API_KEY");
|
|
874
|
+
const { createFalClient } = await import("@fal-ai/client");
|
|
875
|
+
const client2 = createFalClient({ credentials: apiKey });
|
|
876
|
+
const status = await client2.queue.status(endpoint, { requestId });
|
|
877
|
+
if (status.status === "COMPLETED") {
|
|
878
|
+
const result = await client2.queue.result(endpoint, { requestId });
|
|
879
|
+
return { status: "COMPLETED", data: result.data };
|
|
880
|
+
}
|
|
881
|
+
return { status: status.status };
|
|
882
|
+
}
|
|
889
883
|
async prepareImage(imagePath) {
|
|
890
884
|
return uploadToFal(imagePath);
|
|
891
885
|
}
|
|
@@ -894,7 +888,6 @@ var FalProvider = class extends BaseProvider {
|
|
|
894
888
|
// src/providers/vertex.ts
|
|
895
889
|
import { readFile as readFile4 } from "fs/promises";
|
|
896
890
|
import { extname as extname2 } from "path";
|
|
897
|
-
init_model_registry();
|
|
898
891
|
var VERTEX_PROJECT = "gunni-488216";
|
|
899
892
|
var VERTEX_LOCATION = "us-central1";
|
|
900
893
|
function resolveAspectRatio(width, height) {
|
|
@@ -1136,7 +1129,6 @@ async function downloadFromGcs(gcsUri) {
|
|
|
1136
1129
|
|
|
1137
1130
|
// src/providers/openai.ts
|
|
1138
1131
|
import { readFile as readFile5 } from "fs/promises";
|
|
1139
|
-
init_model_registry();
|
|
1140
1132
|
var OPENAI_API_BASE = "https://api.openai.com/v1";
|
|
1141
1133
|
function resolveSize(width, height) {
|
|
1142
1134
|
if (!width || !height) return "1024x1024";
|
|
@@ -1369,16 +1361,9 @@ var ModelRouter = class {
|
|
|
1369
1361
|
}
|
|
1370
1362
|
};
|
|
1371
1363
|
|
|
1372
|
-
// src/commands/image.ts
|
|
1373
|
-
init_model_registry();
|
|
1374
|
-
|
|
1375
|
-
// src/core/provider-resolver.ts
|
|
1376
|
-
init_model_registry();
|
|
1377
|
-
|
|
1378
1364
|
// src/providers/google.ts
|
|
1379
1365
|
import { readFile as readFile6 } from "fs/promises";
|
|
1380
1366
|
import { extname as extname3 } from "path";
|
|
1381
|
-
init_model_registry();
|
|
1382
1367
|
function imageExtToMimeType2(imagePath) {
|
|
1383
1368
|
const ext = extname3(imagePath).toLowerCase();
|
|
1384
1369
|
const map = {
|
|
@@ -1630,6 +1615,8 @@ function openFile(filePath) {
|
|
|
1630
1615
|
detached: true,
|
|
1631
1616
|
stdio: "ignore"
|
|
1632
1617
|
});
|
|
1618
|
+
child.on("error", () => {
|
|
1619
|
+
});
|
|
1633
1620
|
child.unref();
|
|
1634
1621
|
} catch {
|
|
1635
1622
|
}
|
|
@@ -1642,6 +1629,8 @@ function openUrl(url) {
|
|
|
1642
1629
|
detached: true,
|
|
1643
1630
|
stdio: "ignore"
|
|
1644
1631
|
});
|
|
1632
|
+
child.on("error", () => {
|
|
1633
|
+
});
|
|
1645
1634
|
child.unref();
|
|
1646
1635
|
} catch {
|
|
1647
1636
|
}
|
|
@@ -1681,6 +1670,7 @@ async function searchHistory(query) {
|
|
|
1681
1670
|
// src/core/remote-client.ts
|
|
1682
1671
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1683
1672
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1673
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
1684
1674
|
var RemoteClient = class {
|
|
1685
1675
|
client = null;
|
|
1686
1676
|
async connect(apiKey, serverUrl) {
|
|
@@ -1692,14 +1682,18 @@ var RemoteClient = class {
|
|
|
1692
1682
|
}
|
|
1693
1683
|
}
|
|
1694
1684
|
);
|
|
1695
|
-
this.client = new Client({ name: "gunni-cli", version: "0.
|
|
1685
|
+
this.client = new Client({ name: "gunni-cli", version: "0.3.3" });
|
|
1696
1686
|
await this.client.connect(transport);
|
|
1697
1687
|
}
|
|
1698
1688
|
async callTool(name, args) {
|
|
1699
1689
|
if (!this.client) {
|
|
1700
1690
|
throw new GunniError("Remote client not connected", "NOT_CONNECTED");
|
|
1701
1691
|
}
|
|
1702
|
-
const result = await this.client.callTool(
|
|
1692
|
+
const result = await this.client.callTool(
|
|
1693
|
+
{ name, arguments: args },
|
|
1694
|
+
void 0,
|
|
1695
|
+
{ timeout: DEFAULT_TIMEOUT_MS }
|
|
1696
|
+
);
|
|
1703
1697
|
const firstContent = result.content?.[0];
|
|
1704
1698
|
if (!firstContent || firstContent.type !== "text") {
|
|
1705
1699
|
throw new GunniError(
|
|
@@ -1707,7 +1701,20 @@ var RemoteClient = class {
|
|
|
1707
1701
|
"INVALID_RESPONSE"
|
|
1708
1702
|
);
|
|
1709
1703
|
}
|
|
1710
|
-
|
|
1704
|
+
const text3 = firstContent.text;
|
|
1705
|
+
if (result.isError) {
|
|
1706
|
+
try {
|
|
1707
|
+
const parsed = JSON.parse(text3);
|
|
1708
|
+
throw new GunniError(
|
|
1709
|
+
parsed.error?.message ?? parsed.error ?? text3,
|
|
1710
|
+
parsed.error?.code ?? "REMOTE_ERROR"
|
|
1711
|
+
);
|
|
1712
|
+
} catch (e) {
|
|
1713
|
+
if (e instanceof GunniError) throw e;
|
|
1714
|
+
throw new GunniError(text3, "REMOTE_ERROR");
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return JSON.parse(text3);
|
|
1711
1718
|
}
|
|
1712
1719
|
async disconnect() {
|
|
1713
1720
|
if (this.client) {
|
|
@@ -2421,7 +2428,6 @@ async function handleRemoveBg(imagePath, modelId, opts, json) {
|
|
|
2421
2428
|
// src/commands/video.ts
|
|
2422
2429
|
import { existsSync as existsSync2 } from "fs";
|
|
2423
2430
|
import ora3 from "ora";
|
|
2424
|
-
init_model_registry();
|
|
2425
2431
|
import pc4 from "picocolors";
|
|
2426
2432
|
var DEFAULT_MODEL = "kling-v3-pro";
|
|
2427
2433
|
function registerVideoCommand(program2) {
|
|
@@ -2598,13 +2604,20 @@ async function handleRemote2(input, opts, json, apiKey, serverUrl) {
|
|
|
2598
2604
|
"MISSING_INPUT"
|
|
2599
2605
|
);
|
|
2600
2606
|
}
|
|
2601
|
-
|
|
2607
|
+
const looksLikeUrl = /^https?:\/\//.test(input);
|
|
2608
|
+
if (!existsSync2(input) && !looksLikeUrl) {
|
|
2609
|
+
prompt = opts.prompt ? `${input} ${opts.prompt}` : input;
|
|
2610
|
+
if (!userExplicitModel && modelId === DEFAULT_MODEL) {
|
|
2611
|
+
modelId = "kling-v3-pro-t2v";
|
|
2612
|
+
}
|
|
2613
|
+
} else if (existsSync2(input)) {
|
|
2602
2614
|
update("Uploading image\u2026");
|
|
2603
2615
|
imageUrl = await uploadLocalFile(input, apiKey, serverUrl);
|
|
2616
|
+
prompt = opts.prompt ?? "";
|
|
2604
2617
|
} else {
|
|
2605
2618
|
imageUrl = input;
|
|
2619
|
+
prompt = opts.prompt ?? "";
|
|
2606
2620
|
}
|
|
2607
|
-
prompt = opts.prompt ?? "";
|
|
2608
2621
|
}
|
|
2609
2622
|
if (opts.style) {
|
|
2610
2623
|
const customerId = process.env.GUNNI_CUSTOMER_ID;
|
|
@@ -2636,7 +2649,22 @@ async function handleRemote2(input, opts, json, apiKey, serverUrl) {
|
|
|
2636
2649
|
update("Generating video\u2026");
|
|
2637
2650
|
const client2 = new RemoteClient();
|
|
2638
2651
|
await client2.connect(apiKey, serverUrl);
|
|
2639
|
-
|
|
2652
|
+
let result = await client2.callTool("video", toolArgs);
|
|
2653
|
+
if (result.jobId) {
|
|
2654
|
+
const jobId = result.jobId;
|
|
2655
|
+
let jobStatus;
|
|
2656
|
+
while (true) {
|
|
2657
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
2658
|
+
jobStatus = await client2.callTool("job_status", { job_id: jobId });
|
|
2659
|
+
if (jobStatus.status === "completed") break;
|
|
2660
|
+
if (jobStatus.status === "failed") {
|
|
2661
|
+
await client2.disconnect();
|
|
2662
|
+
throw new GunniError(jobStatus.error ?? "Job failed", "JOB_FAILED");
|
|
2663
|
+
}
|
|
2664
|
+
update(`Generating video\u2026 (${jobStatus.status})`);
|
|
2665
|
+
}
|
|
2666
|
+
result = jobStatus?.result ?? {};
|
|
2667
|
+
}
|
|
2640
2668
|
await client2.disconnect();
|
|
2641
2669
|
const requestedPath = opts.output ?? `gunni-video-${Date.now()}.mp4`;
|
|
2642
2670
|
const videoAssetUrl = result.video?.assetUrl;
|
|
@@ -2678,7 +2706,6 @@ async function handleRemote2(input, opts, json, apiKey, serverUrl) {
|
|
|
2678
2706
|
|
|
2679
2707
|
// src/commands/audio.ts
|
|
2680
2708
|
import ora4 from "ora";
|
|
2681
|
-
init_model_registry();
|
|
2682
2709
|
import pc5 from "picocolors";
|
|
2683
2710
|
var DEFAULT_MODEL2 = "minimax-speech";
|
|
2684
2711
|
function registerAudioCommand(program2) {
|
|
@@ -2707,14 +2734,14 @@ Examples:
|
|
|
2707
2734
|
Available audio models: minimax-speech (default), elevenlabs-tts
|
|
2708
2735
|
Run 'gunni list models --type audio' for details.
|
|
2709
2736
|
`
|
|
2710
|
-
).action(async (
|
|
2737
|
+
).action(async (text3, opts) => {
|
|
2711
2738
|
const json = program2.opts().json ?? false;
|
|
2712
2739
|
try {
|
|
2713
2740
|
const config = new ConfigManager();
|
|
2714
2741
|
const gunniKey = await config.getGunniApiKey();
|
|
2715
2742
|
if (gunniKey) {
|
|
2716
2743
|
const serverUrl = await config.getServerUrl();
|
|
2717
|
-
return handleRemote3(
|
|
2744
|
+
return handleRemote3(text3, opts, json, gunniKey, serverUrl);
|
|
2718
2745
|
}
|
|
2719
2746
|
throw new GunniError(
|
|
2720
2747
|
"No API key configured",
|
|
@@ -2729,7 +2756,7 @@ Run 'gunni list models --type audio' for details.
|
|
|
2729
2756
|
const endpoint = getEndpoint(modelId);
|
|
2730
2757
|
const falInput = {};
|
|
2731
2758
|
if (modelId === "minimax-speech") {
|
|
2732
|
-
falInput.prompt =
|
|
2759
|
+
falInput.prompt = text3;
|
|
2733
2760
|
falInput.voice_setting = {
|
|
2734
2761
|
voice_id: opts.voice ?? "Wise_Woman",
|
|
2735
2762
|
speed: 1,
|
|
@@ -2737,10 +2764,10 @@ Run 'gunni list models --type audio' for details.
|
|
|
2737
2764
|
pitch: 0
|
|
2738
2765
|
};
|
|
2739
2766
|
} else if (modelId === "elevenlabs-tts") {
|
|
2740
|
-
falInput.text =
|
|
2767
|
+
falInput.text = text3;
|
|
2741
2768
|
if (opts.voice) falInput.voice_id = opts.voice;
|
|
2742
2769
|
} else {
|
|
2743
|
-
falInput.text =
|
|
2770
|
+
falInput.text = text3;
|
|
2744
2771
|
if (opts.voice) {
|
|
2745
2772
|
falInput.audio_url = opts.voice;
|
|
2746
2773
|
}
|
|
@@ -2761,7 +2788,7 @@ Run 'gunni list models --type audio' for details.
|
|
|
2761
2788
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2762
2789
|
command: "audio",
|
|
2763
2790
|
model: modelId,
|
|
2764
|
-
prompt:
|
|
2791
|
+
prompt: text3,
|
|
2765
2792
|
outputPath: finalPath,
|
|
2766
2793
|
outputUrl: audioUrl
|
|
2767
2794
|
}).catch(() => {
|
|
@@ -2769,7 +2796,7 @@ Run 'gunni list models --type audio' for details.
|
|
|
2769
2796
|
const output = {
|
|
2770
2797
|
audio: { path: finalPath, url: audioUrl },
|
|
2771
2798
|
model: modelId,
|
|
2772
|
-
text:
|
|
2799
|
+
text: text3,
|
|
2773
2800
|
voice: opts.voice
|
|
2774
2801
|
};
|
|
2775
2802
|
if (json) {
|
|
@@ -2796,14 +2823,14 @@ function getEndpoint(modelId) {
|
|
|
2796
2823
|
}
|
|
2797
2824
|
return model.endpoint;
|
|
2798
2825
|
}
|
|
2799
|
-
async function handleRemote3(
|
|
2826
|
+
async function handleRemote3(text3, opts, json, apiKey, serverUrl) {
|
|
2800
2827
|
const spinner = json ? void 0 : ora4("Generating audio\u2026").start();
|
|
2801
2828
|
const update = (s) => {
|
|
2802
2829
|
if (spinner) spinner.text = s;
|
|
2803
2830
|
};
|
|
2804
2831
|
try {
|
|
2805
2832
|
const modelId = opts.model ?? DEFAULT_MODEL2;
|
|
2806
|
-
const toolArgs = { text:
|
|
2833
|
+
const toolArgs = { text: text3, model: modelId };
|
|
2807
2834
|
if (opts.voice) toolArgs.voice = opts.voice;
|
|
2808
2835
|
const client2 = new RemoteClient();
|
|
2809
2836
|
await client2.connect(apiKey, serverUrl);
|
|
@@ -2821,7 +2848,7 @@ async function handleRemote3(text4, opts, json, apiKey, serverUrl) {
|
|
|
2821
2848
|
const output = {
|
|
2822
2849
|
audio: { path: finalPath, url: audioAssetUrl },
|
|
2823
2850
|
model: modelId,
|
|
2824
|
-
text:
|
|
2851
|
+
text: text3,
|
|
2825
2852
|
voice: opts.voice
|
|
2826
2853
|
};
|
|
2827
2854
|
if (json) {
|
|
@@ -2841,7 +2868,6 @@ async function handleRemote3(text4, opts, json, apiKey, serverUrl) {
|
|
|
2841
2868
|
// src/commands/lipsync.ts
|
|
2842
2869
|
import { existsSync as existsSync3 } from "fs";
|
|
2843
2870
|
import ora5 from "ora";
|
|
2844
|
-
init_model_registry();
|
|
2845
2871
|
import pc6 from "picocolors";
|
|
2846
2872
|
var DEFAULT_MODEL3 = "kling-lipsync";
|
|
2847
2873
|
function registerLipsyncCommand(program2) {
|
|
@@ -2992,7 +3018,22 @@ async function handleRemote4(audio, opts, json, apiKey, serverUrl) {
|
|
|
2992
3018
|
update("Generating lip sync\u2026");
|
|
2993
3019
|
const client2 = new RemoteClient();
|
|
2994
3020
|
await client2.connect(apiKey, serverUrl);
|
|
2995
|
-
|
|
3021
|
+
let result = await client2.callTool("lipsync", toolArgs);
|
|
3022
|
+
if (result.jobId) {
|
|
3023
|
+
const jobId = result.jobId;
|
|
3024
|
+
let jobStatus;
|
|
3025
|
+
while (true) {
|
|
3026
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
3027
|
+
jobStatus = await client2.callTool("job_status", { job_id: jobId });
|
|
3028
|
+
if (jobStatus.status === "completed") break;
|
|
3029
|
+
if (jobStatus.status === "failed") {
|
|
3030
|
+
await client2.disconnect();
|
|
3031
|
+
throw new GunniError(jobStatus.error ?? "Job failed", "JOB_FAILED");
|
|
3032
|
+
}
|
|
3033
|
+
update(`Generating lip sync\u2026 (${jobStatus.status})`);
|
|
3034
|
+
}
|
|
3035
|
+
result = jobStatus?.result ?? {};
|
|
3036
|
+
}
|
|
2996
3037
|
await client2.disconnect();
|
|
2997
3038
|
const videoAssetUrl = result.video?.assetUrl;
|
|
2998
3039
|
if (!videoAssetUrl) {
|
|
@@ -3092,34 +3133,18 @@ function formatEntry(e) {
|
|
|
3092
3133
|
// src/commands/learn.ts
|
|
3093
3134
|
import pc8 from "picocolors";
|
|
3094
3135
|
|
|
3095
|
-
//
|
|
3096
|
-
var
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
MULTI-IMAGE: pass an array of URLs to image for multi-ref editing
|
|
3108
|
-
|
|
3109
|
-
USER IMAGE UPLOADS: When a user provides or pastes an image and you need to use it as input (for editing, video, describe, etc.), you MUST call upload_ref first. The image/video tools require a URL (https://...), not a local file path. Workflow:
|
|
3110
|
-
1. Call upload_ref with a name for the reference
|
|
3111
|
-
2. The user uploads their file via the Gunni Studio UI
|
|
3112
|
-
3. After upload completes, use the resulting URL in image/video tools
|
|
3113
|
-
Never pass a local file path to image or video. It will fail.
|
|
3114
|
-
|
|
3115
|
-
PROJECT SYSTEM: BRIEF.md (creative direction), .gunni/assets.json (lineage), .gunni/seeds.json (proven seeds)
|
|
3116
|
-
|
|
3117
|
-
IMPORTANT: Every generation response includes a galleryUrl. Always share it with the user so they can view results in full resolution.`
|
|
3118
|
-
},
|
|
3119
|
-
exploration: {
|
|
3120
|
-
topic: "exploration",
|
|
3121
|
-
title: "Creative Exploration",
|
|
3122
|
-
content: `Creative work is a tree, not a pipeline. Here's how to explore effectively:
|
|
3136
|
+
// data/knowledge.json
|
|
3137
|
+
var knowledge_default = {
|
|
3138
|
+
topics: {
|
|
3139
|
+
overview: {
|
|
3140
|
+
topic: "overview",
|
|
3141
|
+
title: "Gunni Overview",
|
|
3142
|
+
content: "Gunni tools for professional media creation:\n\nMEDIA: image (generate/edit/describe/upscale/remove-bg, routes by input), video, audio\nREFS: ref_save (save image as ref), refs (list or get refs by name), upload_ref (user uploads from their device)\nUTILITY: models (list models + updates), history (search past work), learn (you're here)\n\nROUTING (image): prompt only \u2192 generate, image+prompt \u2192 edit, image only \u2192 describe, +upscale/remove_bg flags\nMULTI-IMAGE: pass an array of URLs to image for multi-ref editing\n\nUSER IMAGE UPLOADS: When a user provides or pastes an image and you need to use it as input (for editing, video, describe, etc.), you MUST call upload_ref first. The image/video tools require a URL (https://...), not a local file path. Workflow:\n1. Call upload_ref with a name for the reference\n2. The user uploads their file via the Gunni Studio UI\n3. After upload completes, use the resulting URL in image/video tools\nNever pass a local file path to image or video. It will fail.\n\nPROJECT SYSTEM: BRIEF.md (creative direction), .gunni/assets.json (lineage), .gunni/seeds.json (proven seeds)\n\nIMPORTANT: Every generation response includes a galleryUrl. Always share it with the user so they can view results in full resolution."
|
|
3143
|
+
},
|
|
3144
|
+
exploration: {
|
|
3145
|
+
topic: "exploration",
|
|
3146
|
+
title: "Creative Exploration",
|
|
3147
|
+
content: `Creative work is a tree, not a pipeline. Here's how to explore effectively:
|
|
3123
3148
|
|
|
3124
3149
|
ROUND 1: START FOCUSED
|
|
3125
3150
|
- Generate 1 image first. Show it to the user and get feedback before making more.
|
|
@@ -3145,91 +3170,21 @@ POLISH
|
|
|
3145
3170
|
- Don't upscale until the concept is locked. Upscaling is the final step.
|
|
3146
3171
|
- Use image --upscale for production quality.
|
|
3147
3172
|
- Describe the final result to verify it meets the brief.`
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
- Mood: "warm", "cinematic", "moody", "bright and airy", "dramatic lighting"
|
|
3164
|
-
- Quality: "high detail", "photorealistic", "4K", "professional grade"
|
|
3165
|
-
- Camera: "wide-angle", "f/2.8", "85mm portrait lens", "macro", "low angle shot", "dolly zoom"
|
|
3166
|
-
|
|
3167
|
-
NEGATIVE PROMPTS
|
|
3168
|
-
- Explicitly exclude unwanted elements: "no blur, no artifacts, no text"
|
|
3169
|
-
- Effective for cleaning up outputs, especially with Recraft v4 and Flux.
|
|
3170
|
-
- Example: "professional headshot, studio lighting, no watermark, no distortion"
|
|
3171
|
-
|
|
3172
|
-
CONSISTENCY
|
|
3173
|
-
- Use seeds for reproducible results. Same seed + same prompt = same image.
|
|
3174
|
-
- When creating a series, keep the style keywords consistent across prompts.
|
|
3175
|
-
- Reference BRIEF.md constraints to stay on-brand.
|
|
3176
|
-
|
|
3177
|
-
FOR EDITS
|
|
3178
|
-
- Be specific about what to change: "make the lighting warmer" not "improve it".
|
|
3179
|
-
- Reference specific elements: "change the background to marble" not "change the background".
|
|
3180
|
-
- Additive edits work better than subtractive ones.
|
|
3181
|
-
- For faces/people: use "preserve facial features" or "maintain likeness" to keep identity.
|
|
3182
|
-
- One edit at a time yields better results than compound changes.
|
|
3183
|
-
|
|
3184
|
-
REFERENCES
|
|
3185
|
-
- High-quality references (2K/4K) prevent artifacts like blurriness.
|
|
3186
|
-
- Tag inputs clearly for multi-image edits: @image1 for character, @image2 for style.
|
|
3187
|
-
|
|
3188
|
-
MODEL STRENGTHS
|
|
3189
|
-
- Nano Banana: Best overall quality. Photorealism, complex scenes, multi-turn edits. Use "preserve facial features" for likeness consistency.
|
|
3190
|
-
- Recraft v4: Text rendering, design, vectors, marketing materials. Use negative prompts for text-heavy designs. Only model that reliably puts text in images.
|
|
3191
|
-
- Flux 2 Pro / Flux Kontext: Technical descriptions over vague adjectives. Specify camera settings (lens, aperture, angle) for best results. Kontext excels at targeted local edits.
|
|
3192
|
-
- GPT Image: Strong text rendering and surgical inpainting. Good for precise modifications.
|
|
3193
|
-
- Use default models unless you have a specific reason to override.`
|
|
3194
|
-
},
|
|
3195
|
-
brand: {
|
|
3196
|
-
topic: "brand",
|
|
3197
|
-
title: "Brand Identity Design",
|
|
3198
|
-
content: `How to create a cohesive brand identity with Gunni:
|
|
3199
|
-
|
|
3200
|
-
START WITH THE BRIEF
|
|
3201
|
-
- Read BRIEF.md for colors, style, constraints, and references.
|
|
3202
|
-
- If no BRIEF.md exists, ask what the brand represents before generating.
|
|
3203
|
-
|
|
3204
|
-
LOGO EXPLORATION
|
|
3205
|
-
- Start with 1 logo concept. Show it, get feedback, then iterate or try a different approach.
|
|
3206
|
-
- Only generate multiple variants when the user asks for options.
|
|
3207
|
-
- ALWAYS include the exact brand/company name in the prompt when the logo should contain text.
|
|
3208
|
-
- Use Recraft v4 for logos that include text (only model that nails typography).
|
|
3209
|
-
- Use Nano Banana for abstract marks and iconic logos.
|
|
3210
|
-
- Always remove background after selecting a logo direction.
|
|
3211
|
-
|
|
3212
|
-
COLOR PALETTE
|
|
3213
|
-
- Extract colors from the best generation and codify them in BRIEF.md.
|
|
3214
|
-
- Use consistent color keywords in all subsequent prompts.
|
|
3215
|
-
- Test colors across light and dark backgrounds.
|
|
3216
|
-
|
|
3217
|
-
BUILDING THE KIT
|
|
3218
|
-
1. Logo (with and without background)
|
|
3219
|
-
2. Hero image (wide format, brand aesthetic)
|
|
3220
|
-
3. Social media templates (square, story, header sizes)
|
|
3221
|
-
4. Pattern or texture (for backgrounds)
|
|
3222
|
-
5. Product mockups (logo placed in context)
|
|
3223
|
-
|
|
3224
|
-
CONSISTENCY
|
|
3225
|
-
- Save good seeds to seeds.json after each successful generation.
|
|
3226
|
-
- Reference the logo in subsequent prompts: "in the style of [brand], using [colors]"
|
|
3227
|
-
- Keep style keywords identical across all generations.`
|
|
3228
|
-
},
|
|
3229
|
-
"ui-design": {
|
|
3230
|
-
topic: "ui-design",
|
|
3231
|
-
title: "UI/UX Design",
|
|
3232
|
-
content: `How to generate UI designs and interface assets with Gunni:
|
|
3173
|
+
},
|
|
3174
|
+
prompting: {
|
|
3175
|
+
topic: "prompting",
|
|
3176
|
+
title: "Prompt Craft",
|
|
3177
|
+
content: 'How to write effective prompts for image generation:\n\nSTRUCTURE\n- Lead with the subject, then add style, lighting, and mood.\n- Be specific: subject + action + setting + style + technical elements.\n- Good: "ceramic mug on wooden table, morning light, editorial photography, shallow depth of field"\n- Weak: "a nice mug photo"\n\nSTYLE KEYWORDS THAT WORK\n- Photography: "editorial", "product photography", "studio lighting", "natural light", "shallow depth of field"\n- Design: "minimalist", "clean lines", "modern", "professional", "flat design"\n- Mood: "warm", "cinematic", "moody", "bright and airy", "dramatic lighting"\n- Quality: "high detail", "photorealistic", "4K", "professional grade"\n- Camera: "wide-angle", "f/2.8", "85mm portrait lens", "macro", "low angle shot", "dolly zoom"\n\nNEGATIVE PROMPTS\n- Explicitly exclude unwanted elements: "no blur, no artifacts, no text"\n- Effective for cleaning up outputs, especially with Recraft v4 and Flux.\n- Example: "professional headshot, studio lighting, no watermark, no distortion"\n\nCONSISTENCY\n- Use seeds for reproducible results. Same seed + same prompt = same image.\n- When creating a series, keep the style keywords consistent across prompts.\n- Reference BRIEF.md constraints to stay on-brand.\n\nFOR EDITS\n- Be specific about what to change: "make the lighting warmer" not "improve it".\n- Reference specific elements: "change the background to marble" not "change the background".\n- Additive edits work better than subtractive ones.\n- For faces/people: use "preserve facial features" or "maintain likeness" to keep identity.\n- One edit at a time yields better results than compound changes.\n\nREFERENCES\n- High-quality references (2K/4K) prevent artifacts like blurriness.\n- Tag inputs clearly for multi-image edits: @image1 for character, @image2 for style.\n\nMODEL STRENGTHS\n- Nano Banana: Best overall quality. Photorealism, complex scenes, multi-turn edits. Use "preserve facial features" for likeness consistency.\n- Recraft v4: Text rendering, design, vectors, marketing materials. Use negative prompts for text-heavy designs. Only model that reliably puts text in images.\n- Flux 2 Pro / Flux Kontext: Technical descriptions over vague adjectives. Specify camera settings (lens, aperture, angle) for best results. Kontext excels at targeted local edits.\n- GPT Image: Strong text rendering and surgical inpainting. Good for precise modifications.\n- Use default models unless you have a specific reason to override.'
|
|
3178
|
+
},
|
|
3179
|
+
brand: {
|
|
3180
|
+
topic: "brand",
|
|
3181
|
+
title: "Brand Identity Design",
|
|
3182
|
+
content: 'How to create a cohesive brand identity with Gunni:\n\nSTART WITH THE BRIEF\n- Read BRIEF.md for colors, style, constraints, and references.\n- If no BRIEF.md exists, ask what the brand represents before generating.\n\nLOGO EXPLORATION\n- Start with 1 logo concept. Show it, get feedback, then iterate or try a different approach.\n- Only generate multiple variants when the user asks for options.\n- ALWAYS include the exact brand/company name in the prompt when the logo should contain text.\n- Use Recraft v4 for logos that include text (only model that nails typography).\n- Use Nano Banana for abstract marks and iconic logos.\n- Always remove background after selecting a logo direction.\n\nCOLOR PALETTE\n- Extract colors from the best generation and codify them in BRIEF.md.\n- Use consistent color keywords in all subsequent prompts.\n- Test colors across light and dark backgrounds.\n\nBUILDING THE KIT\n1. Logo (with and without background)\n2. Hero image (wide format, brand aesthetic)\n3. Social media templates (square, story, header sizes)\n4. Pattern or texture (for backgrounds)\n5. Product mockups (logo placed in context)\n\nCONSISTENCY\n- Save good seeds to seeds.json after each successful generation.\n- Reference the logo in subsequent prompts: "in the style of [brand], using [colors]"\n- Keep style keywords identical across all generations.'
|
|
3183
|
+
},
|
|
3184
|
+
"ui-design": {
|
|
3185
|
+
topic: "ui-design",
|
|
3186
|
+
title: "UI/UX Design",
|
|
3187
|
+
content: `How to generate UI designs and interface assets with Gunni:
|
|
3233
3188
|
|
|
3234
3189
|
SCREEN GENERATION
|
|
3235
3190
|
- Nano Banana excels at generating realistic UI screenshots and mockups.
|
|
@@ -3255,143 +3210,31 @@ TIPS
|
|
|
3255
3210
|
- Reference specific apps in prompts: "in the style of Linear" or "inspired by Stripe's dashboard"
|
|
3256
3211
|
- For responsive design, generate the same screen at different widths.
|
|
3257
3212
|
- Use describe to document what each screen shows for handoff.`
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
-
|
|
3275
|
-
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
-
|
|
3283
|
-
|
|
3284
|
-
VIDEO ADS
|
|
3285
|
-
- Generate a compelling still, then create a 5-10 second video.
|
|
3286
|
-
- Add voiceover with gunni audio for complete ad packages.
|
|
3287
|
-
- Different video styles: zoom in, pan, parallax, subtle motion.
|
|
3288
|
-
|
|
3289
|
-
TIPS
|
|
3290
|
-
- Ad images should have clear focal points and work at small sizes.
|
|
3291
|
-
- Test thumbnails at actual display size (they're often tiny).
|
|
3292
|
-
- Keep text minimal in generated images. Add text in post-production or use Recraft v4.`
|
|
3293
|
-
},
|
|
3294
|
-
"product-photo": {
|
|
3295
|
-
topic: "product-photo",
|
|
3296
|
-
title: "Product Photography",
|
|
3297
|
-
content: `How to create product photography with Gunni:
|
|
3298
|
-
|
|
3299
|
-
FROM SCRATCH
|
|
3300
|
-
1. Generate the product: "ceramic mug, white background, studio lighting, product photography"
|
|
3301
|
-
2. Remove background: image --remove-bg
|
|
3302
|
-
3. Place in scenes: "place on marble countertop, morning light, coffee beans scattered nearby"
|
|
3303
|
-
4. Upscale the final: image --upscale
|
|
3304
|
-
|
|
3305
|
-
FROM EXISTING PRODUCT PHOTO
|
|
3306
|
-
1. Remove background from the original photo.
|
|
3307
|
-
2. Use edit to place in new contexts and lighting.
|
|
3308
|
-
3. Generate multiple scene variants with --variants.
|
|
3309
|
-
4. Upscale winners.
|
|
3310
|
-
|
|
3311
|
-
LIGHTING STYLES
|
|
3312
|
-
- "Studio lighting" \u2014 clean, professional, white background
|
|
3313
|
-
- "Natural light" \u2014 warm, lifestyle feel
|
|
3314
|
-
- "Dramatic lighting" \u2014 moody, high contrast, dark background
|
|
3315
|
-
- "Flat lay" \u2014 top-down arrangement with props
|
|
3316
|
-
- "Lifestyle" \u2014 product in use, real-world context
|
|
3317
|
-
|
|
3318
|
-
SCENE TYPES
|
|
3319
|
-
- Hero shot (product as star, minimal background)
|
|
3320
|
-
- Lifestyle (product in use context)
|
|
3321
|
-
- Detail (close-up of texture, material, craftsmanship)
|
|
3322
|
-
- Group (multiple products arranged together)
|
|
3323
|
-
- Scale (product next to familiar objects for size reference)
|
|
3324
|
-
|
|
3325
|
-
E-COMMERCE
|
|
3326
|
-
- Main image: white background, centered product, high resolution.
|
|
3327
|
-
- Use describe to generate alt text and product descriptions.
|
|
3328
|
-
- Generate 4-6 angles per product for a complete listing.`
|
|
3329
|
-
},
|
|
3330
|
-
video: {
|
|
3331
|
-
topic: "video",
|
|
3332
|
-
title: "Video Generation",
|
|
3333
|
-
content: `How to create effective video content with Gunni:
|
|
3334
|
-
|
|
3335
|
-
PROMPT STRUCTURE
|
|
3336
|
-
- Include: subject + action + motion style + setting + camera movement.
|
|
3337
|
-
- Good: "golden retriever running through autumn leaves, slow motion, tracking shot, warm afternoon light, cinematic"
|
|
3338
|
-
- Weak: "dog running"
|
|
3339
|
-
|
|
3340
|
-
KLING V3 (default video model)
|
|
3341
|
-
- Structure prompts with subject, action, motion, and negative elements.
|
|
3342
|
-
- Use negative phrases to prevent common issues: "no flicker, no distortion, no blur".
|
|
3343
|
-
- Best for: dynamic scenes with fluid motion, product reveals, character animation.
|
|
3344
|
-
- Keep clips under 10 seconds for best quality.
|
|
3345
|
-
- Start from a still image for more control (image-to-video).
|
|
3346
|
-
|
|
3347
|
-
VEO 3.1
|
|
3348
|
-
- Five-part prompt formula: narrative + characters + visuals + audio + camera controls.
|
|
3349
|
-
- Avoid quotes in prompts (they can trigger unwanted text rendering).
|
|
3350
|
-
- Stick to single scenes in short clips. Use jump-cuts for multi-scene narratives.
|
|
3351
|
-
- Specify camera techniques: "dolly zoom", "tracking shot", "aerial pullback".
|
|
3352
|
-
|
|
3353
|
-
GENERAL VIDEO TIPS
|
|
3354
|
-
- Start with a strong still image first, then animate. More control than text-to-video.
|
|
3355
|
-
- Test with 5-second clips before committing to longer generations.
|
|
3356
|
-
- Camera motion keywords: "static", "slow pan", "tracking", "dolly", "crane", "handheld".
|
|
3357
|
-
- For product videos: start with hero shot, add subtle motion (rotation, parallax, zoom).
|
|
3358
|
-
- For social: keep under 6 seconds, strong first frame, works on mute.`
|
|
3359
|
-
},
|
|
3360
|
-
"concept-art": {
|
|
3361
|
-
topic: "concept-art",
|
|
3362
|
-
title: "Concept Art & Illustration",
|
|
3363
|
-
content: `How to create concept art and illustrations with Gunni:
|
|
3364
|
-
|
|
3365
|
-
ENVIRONMENT DESIGN
|
|
3366
|
-
- Start broad: "Pacific Northwest forest, morning mist, golden hour, cinematic"
|
|
3367
|
-
- Refine with edits: add structures, characters, atmospheric effects.
|
|
3368
|
-
- Use --variants to explore different moods and lighting for the same scene.
|
|
3369
|
-
|
|
3370
|
-
CHARACTER CONCEPTS
|
|
3371
|
-
- Generate full-body character designs with clear descriptions.
|
|
3372
|
-
- Create expression sheets with edit: same character, different expressions.
|
|
3373
|
-
- Use consistent style keywords and seeds across a character series.
|
|
3374
|
-
|
|
3375
|
-
ARCHITECTURAL CONCEPTS
|
|
3376
|
-
- Specify materials, era, and style: "modern lakehouse, cedar and glass, Olson Kundig inspired"
|
|
3377
|
-
- Generate multiple views: exterior, interior, aerial, detail.
|
|
3378
|
-
- Use edit to iterate on specific elements without regenerating the whole scene.
|
|
3379
|
-
|
|
3380
|
-
ITERATION WORKFLOW
|
|
3381
|
-
- Round 1: Generate 1 concept. Show it and get feedback.
|
|
3382
|
-
- Round 2: Refine via edit, or try a different direction if needed.
|
|
3383
|
-
- Round 3: Use 2-3 variants only when exploring mood/lighting options the user requested.
|
|
3384
|
-
- Round 4: Upscale and describe for documentation.
|
|
3385
|
-
|
|
3386
|
-
STYLE CONSISTENCY
|
|
3387
|
-
- Lock in style keywords early and reuse them.
|
|
3388
|
-
- Save the seed from your best generations.
|
|
3389
|
-
- Reference previous work in edit prompts: "maintaining the same art style"`
|
|
3390
|
-
},
|
|
3391
|
-
pipelines: {
|
|
3392
|
-
topic: "pipelines",
|
|
3393
|
-
title: "Pipeline Orchestration",
|
|
3394
|
-
content: `How to execute Gunni pipelines. Pipelines are multi-step workflows that chain templates, presets, and tools into complete deliverables.
|
|
3213
|
+
},
|
|
3214
|
+
advertising: {
|
|
3215
|
+
topic: "advertising",
|
|
3216
|
+
title: "Ad Creative",
|
|
3217
|
+
content: "How to create advertising assets with Gunni:\n\nCAMPAIGN STRUCTURE\n- Start with the hero image: the single strongest visual.\n- Generate platform-specific sizes from the same concept:\n - Instagram: 1080x1080 (feed), 1080x1920 (story)\n - Twitter/X: 1200x675 (post), 1500x500 (header)\n - YouTube: 1280x720 (thumbnail)\n - LinkedIn: 1200x627 (post)\n - OG: 1200x630\n\nA/B TESTING\n- Start with 1 hero image. Only generate additional variants when the user wants to compare options.\n- Keep copy consistent, vary imagery. Or keep imagery consistent, vary composition.\n- Use describe on each to document the differences for the team.\n\nPRODUCT SHOTS\n- Start with a clean product image (generate or use existing).\n- Remove background for a clean cutout.\n- Use edit to place in different lifestyle scenes.\n- Upscale the winners for production use.\n\nVIDEO ADS\n- Generate a compelling still, then create a 5-10 second video.\n- Add voiceover with gunni audio for complete ad packages.\n- Different video styles: zoom in, pan, parallax, subtle motion.\n\nTIPS\n- Ad images should have clear focal points and work at small sizes.\n- Test thumbnails at actual display size (they're often tiny).\n- Keep text minimal in generated images. Add text in post-production or use Recraft v4."
|
|
3218
|
+
},
|
|
3219
|
+
"product-photo": {
|
|
3220
|
+
topic: "product-photo",
|
|
3221
|
+
title: "Product Photography",
|
|
3222
|
+
content: 'How to create product photography with Gunni:\n\nFROM SCRATCH\n1. Generate the product: "ceramic mug, white background, studio lighting, product photography"\n2. Remove background: image --remove-bg\n3. Place in scenes: "place on marble countertop, morning light, coffee beans scattered nearby"\n4. Upscale the final: image --upscale\n\nFROM EXISTING PRODUCT PHOTO\n1. Remove background from the original photo.\n2. Use edit to place in new contexts and lighting.\n3. Generate multiple scene variants with --variants.\n4. Upscale winners.\n\nLIGHTING STYLES\n- "Studio lighting" \u2014 clean, professional, white background\n- "Natural light" \u2014 warm, lifestyle feel\n- "Dramatic lighting" \u2014 moody, high contrast, dark background\n- "Flat lay" \u2014 top-down arrangement with props\n- "Lifestyle" \u2014 product in use, real-world context\n\nSCENE TYPES\n- Hero shot (product as star, minimal background)\n- Lifestyle (product in use context)\n- Detail (close-up of texture, material, craftsmanship)\n- Group (multiple products arranged together)\n- Scale (product next to familiar objects for size reference)\n\nE-COMMERCE\n- Main image: white background, centered product, high resolution.\n- Use describe to generate alt text and product descriptions.\n- Generate 4-6 angles per product for a complete listing.'
|
|
3223
|
+
},
|
|
3224
|
+
video: {
|
|
3225
|
+
topic: "video",
|
|
3226
|
+
title: "Video Generation",
|
|
3227
|
+
content: 'How to create effective video content with Gunni:\n\nPROMPT STRUCTURE\n- Include: subject + action + motion style + setting + camera movement.\n- Good: "golden retriever running through autumn leaves, slow motion, tracking shot, warm afternoon light, cinematic"\n- Weak: "dog running"\n\nKLING V3 (default video model)\n- Structure prompts with subject, action, motion, and negative elements.\n- Use negative phrases to prevent common issues: "no flicker, no distortion, no blur".\n- Best for: dynamic scenes with fluid motion, product reveals, character animation.\n- Keep clips under 10 seconds for best quality.\n- Start from a still image for more control (image-to-video).\n\nVEO 3.1\n- Five-part prompt formula: narrative + characters + visuals + audio + camera controls.\n- Avoid quotes in prompts (they can trigger unwanted text rendering).\n- Stick to single scenes in short clips. Use jump-cuts for multi-scene narratives.\n- Specify camera techniques: "dolly zoom", "tracking shot", "aerial pullback".\n\nGENERAL VIDEO TIPS\n- Start with a strong still image first, then animate. More control than text-to-video.\n- Test with 5-second clips before committing to longer generations.\n- Camera motion keywords: "static", "slow pan", "tracking", "dolly", "crane", "handheld".\n- For product videos: start with hero shot, add subtle motion (rotation, parallax, zoom).\n- For social: keep under 6 seconds, strong first frame, works on mute.'
|
|
3228
|
+
},
|
|
3229
|
+
"concept-art": {
|
|
3230
|
+
topic: "concept-art",
|
|
3231
|
+
title: "Concept Art & Illustration",
|
|
3232
|
+
content: 'How to create concept art and illustrations with Gunni:\n\nENVIRONMENT DESIGN\n- Start broad: "Pacific Northwest forest, morning mist, golden hour, cinematic"\n- Refine with edits: add structures, characters, atmospheric effects.\n- Use --variants to explore different moods and lighting for the same scene.\n\nCHARACTER CONCEPTS\n- Generate full-body character designs with clear descriptions.\n- Create expression sheets with edit: same character, different expressions.\n- Use consistent style keywords and seeds across a character series.\n\nARCHITECTURAL CONCEPTS\n- Specify materials, era, and style: "modern lakehouse, cedar and glass, Olson Kundig inspired"\n- Generate multiple views: exterior, interior, aerial, detail.\n- Use edit to iterate on specific elements without regenerating the whole scene.\n\nITERATION WORKFLOW\n- Round 1: Generate 1 concept. Show it and get feedback.\n- Round 2: Refine via edit, or try a different direction if needed.\n- Round 3: Use 2-3 variants only when exploring mood/lighting options the user requested.\n- Round 4: Upscale and describe for documentation.\n\nSTYLE CONSISTENCY\n- Lock in style keywords early and reuse them.\n- Save the seed from your best generations.\n- Reference previous work in edit prompts: "maintaining the same art style"'
|
|
3233
|
+
},
|
|
3234
|
+
pipelines: {
|
|
3235
|
+
topic: "pipelines",
|
|
3236
|
+
title: "Pipeline Orchestration",
|
|
3237
|
+
content: `How to execute Gunni pipelines. Pipelines are multi-step workflows that chain templates, presets, and tools into complete deliverables.
|
|
3395
3238
|
|
|
3396
3239
|
DISCOVERY
|
|
3397
3240
|
- Call pipelines() to list available pipelines with names and descriptions
|
|
@@ -3437,8 +3280,23 @@ KEY PRINCIPLES
|
|
|
3437
3280
|
- Use refs to chain outputs: step 1 output becomes step 2 input via input_refs.
|
|
3438
3281
|
- The preset auto-applies to every generation. Don't manually add framing/suffix.
|
|
3439
3282
|
- Assembly notes (the text step) should include: edit sequence, scene order, timing, text overlay copy, CTAs.`
|
|
3283
|
+
}
|
|
3284
|
+
},
|
|
3285
|
+
latest: {
|
|
3286
|
+
version: "0.2.0",
|
|
3287
|
+
date: "2026-02-22",
|
|
3288
|
+
updates: [
|
|
3289
|
+
"Unified image tool: generate, edit, describe, upscale, remove-bg. Routes by input.",
|
|
3290
|
+
"Multi-image editing: pass array of URLs for multi-ref edits (flux-kontext, flux-2-pro-edit, nano-banana-edit, gpt-image-edit).",
|
|
3291
|
+
"Ref system: ref_save + refs for saving/retrieving reference images.",
|
|
3292
|
+
"Variants flag for creative exploration.",
|
|
3293
|
+
"Latest updates merged into models (pass updates=true). refs now handles both list and get."
|
|
3294
|
+
]
|
|
3440
3295
|
}
|
|
3441
3296
|
};
|
|
3297
|
+
|
|
3298
|
+
// src/core/knowledge.ts
|
|
3299
|
+
var TOPICS = knowledge_default.topics;
|
|
3442
3300
|
function getLearnContent(topic) {
|
|
3443
3301
|
if (!topic || topic === "overview") {
|
|
3444
3302
|
return TOPICS.overview;
|
|
@@ -3451,17 +3309,7 @@ function getAvailableTopics() {
|
|
|
3451
3309
|
return Object.keys(TOPICS);
|
|
3452
3310
|
}
|
|
3453
3311
|
function getLatestUpdates() {
|
|
3454
|
-
return
|
|
3455
|
-
version: "0.2.0",
|
|
3456
|
-
date: "2026-02-22",
|
|
3457
|
-
updates: [
|
|
3458
|
-
"Unified image tool: generate, edit, describe, upscale, remove-bg. Routes by input.",
|
|
3459
|
-
"Multi-image editing: pass array of URLs for multi-ref edits (flux-kontext, flux-2-pro-edit, nano-banana-edit, gpt-image-edit).",
|
|
3460
|
-
"Ref system: ref_save + refs for saving/retrieving reference images.",
|
|
3461
|
-
"Variants flag for creative exploration.",
|
|
3462
|
-
"Latest updates merged into models (pass updates=true). refs now handles both list and get."
|
|
3463
|
-
]
|
|
3464
|
-
};
|
|
3312
|
+
return knowledge_default.latest;
|
|
3465
3313
|
}
|
|
3466
3314
|
|
|
3467
3315
|
// src/commands/learn.ts
|
|
@@ -3852,12 +3700,10 @@ async function interactiveConfig(configManager, json) {
|
|
|
3852
3700
|
}
|
|
3853
3701
|
|
|
3854
3702
|
// src/commands/list.ts
|
|
3855
|
-
init_model_registry();
|
|
3856
3703
|
import pc12 from "picocolors";
|
|
3857
3704
|
|
|
3858
|
-
//
|
|
3859
|
-
var
|
|
3860
|
-
// --- Branding ---
|
|
3705
|
+
// data/guides.json
|
|
3706
|
+
var guides_default = [
|
|
3861
3707
|
{
|
|
3862
3708
|
id: "brand-moodboard",
|
|
3863
3709
|
title: "Brand Mood Board",
|
|
@@ -3897,15 +3743,7 @@ var GUIDES = [
|
|
|
3897
3743
|
description: "Upscale the final mood board to presentation quality."
|
|
3898
3744
|
}
|
|
3899
3745
|
],
|
|
3900
|
-
tags: [
|
|
3901
|
-
"brand",
|
|
3902
|
-
"moodboard",
|
|
3903
|
-
"mood-board",
|
|
3904
|
-
"identity",
|
|
3905
|
-
"grid",
|
|
3906
|
-
"palette",
|
|
3907
|
-
"branding"
|
|
3908
|
-
]
|
|
3746
|
+
tags: ["brand", "moodboard", "mood-board", "identity", "grid", "palette", "branding"]
|
|
3909
3747
|
},
|
|
3910
3748
|
{
|
|
3911
3749
|
id: "logo-concepts",
|
|
@@ -3948,15 +3786,7 @@ var GUIDES = [
|
|
|
3948
3786
|
description: "Upscale final logo to high resolution."
|
|
3949
3787
|
}
|
|
3950
3788
|
],
|
|
3951
|
-
tags: [
|
|
3952
|
-
"logo",
|
|
3953
|
-
"brand",
|
|
3954
|
-
"identity",
|
|
3955
|
-
"mark",
|
|
3956
|
-
"wordmark",
|
|
3957
|
-
"vector",
|
|
3958
|
-
"design"
|
|
3959
|
-
]
|
|
3789
|
+
tags: ["logo", "brand", "identity", "mark", "wordmark", "vector", "design"]
|
|
3960
3790
|
},
|
|
3961
3791
|
{
|
|
3962
3792
|
id: "color-palette-exploration",
|
|
@@ -3990,28 +3820,14 @@ var GUIDES = [
|
|
|
3990
3820
|
description: "Generate a pattern using the palette to test it in a graphic design context."
|
|
3991
3821
|
}
|
|
3992
3822
|
],
|
|
3993
|
-
tags: [
|
|
3994
|
-
"color",
|
|
3995
|
-
"palette",
|
|
3996
|
-
"colors",
|
|
3997
|
-
"swatches",
|
|
3998
|
-
"scheme",
|
|
3999
|
-
"brand",
|
|
4000
|
-
"design"
|
|
4001
|
-
]
|
|
3823
|
+
tags: ["color", "palette", "colors", "swatches", "scheme", "brand", "design"]
|
|
4002
3824
|
},
|
|
4003
|
-
// --- Product ---
|
|
4004
3825
|
{
|
|
4005
3826
|
id: "product-photography",
|
|
4006
3827
|
title: "Product Photography Pipeline",
|
|
4007
3828
|
description: "Generate a product shot, remove its background, composite onto a scene, and upscale for production use.",
|
|
4008
3829
|
category: "product",
|
|
4009
|
-
models: [
|
|
4010
|
-
"flux-2-pro",
|
|
4011
|
-
"bria-bg-remove",
|
|
4012
|
-
"flux-kontext",
|
|
4013
|
-
"topaz-upscale"
|
|
4014
|
-
],
|
|
3830
|
+
models: ["flux-2-pro", "bria-bg-remove", "flux-kontext", "topaz-upscale"],
|
|
4015
3831
|
promptTemplate: "Professional product photography of {product_description}. {lighting_style} lighting, {surface_description} surface, {angle} angle. Sharp focus, commercial quality, 8K detail.",
|
|
4016
3832
|
tips: [
|
|
4017
3833
|
"Flux 2 Pro produces the most photorealistic product shots out of the box.",
|
|
@@ -4049,16 +3865,8 @@ var GUIDES = [
|
|
|
4049
3865
|
description: "Verify the final image quality and composition before delivery."
|
|
4050
3866
|
}
|
|
4051
3867
|
],
|
|
4052
|
-
tags: [
|
|
4053
|
-
"product",
|
|
4054
|
-
"photography",
|
|
4055
|
-
"ecommerce",
|
|
4056
|
-
"packshot",
|
|
4057
|
-
"studio",
|
|
4058
|
-
"commercial"
|
|
4059
|
-
]
|
|
3868
|
+
tags: ["product", "photography", "ecommerce", "packshot", "studio", "commercial"]
|
|
4060
3869
|
},
|
|
4061
|
-
// --- Social Media ---
|
|
4062
3870
|
{
|
|
4063
3871
|
id: "social-media-assets",
|
|
4064
3872
|
title: "Social Media Asset Creation",
|
|
@@ -4094,29 +3902,14 @@ var GUIDES = [
|
|
|
4094
3902
|
description: "Analyze the generated asset to verify brand alignment and visual quality."
|
|
4095
3903
|
}
|
|
4096
3904
|
],
|
|
4097
|
-
tags: [
|
|
4098
|
-
"social",
|
|
4099
|
-
"instagram",
|
|
4100
|
-
"facebook",
|
|
4101
|
-
"twitter",
|
|
4102
|
-
"story",
|
|
4103
|
-
"post",
|
|
4104
|
-
"banner",
|
|
4105
|
-
"marketing"
|
|
4106
|
-
]
|
|
3905
|
+
tags: ["social", "instagram", "facebook", "twitter", "story", "post", "banner", "marketing"]
|
|
4107
3906
|
},
|
|
4108
|
-
// --- Iteration ---
|
|
4109
3907
|
{
|
|
4110
3908
|
id: "image-refinement",
|
|
4111
3909
|
title: "Image Refinement and Iteration",
|
|
4112
3910
|
description: "Systematically improve an image through describe-edit-verify cycles.",
|
|
4113
3911
|
category: "iteration",
|
|
4114
|
-
models: [
|
|
4115
|
-
"florence-2",
|
|
4116
|
-
"flux-kontext",
|
|
4117
|
-
"nano-banana-edit",
|
|
4118
|
-
"topaz-upscale"
|
|
4119
|
-
],
|
|
3912
|
+
models: ["florence-2", "flux-kontext", "nano-banana-edit", "topaz-upscale"],
|
|
4120
3913
|
promptTemplate: "This is a refinement workflow, not a single prompt. Start with gunni describe, then edit based on the analysis.",
|
|
4121
3914
|
tips: [
|
|
4122
3915
|
"Always describe before editing \u2014 the description reveals issues you might not notice visually.",
|
|
@@ -4156,17 +3949,8 @@ var GUIDES = [
|
|
|
4156
3949
|
description: "Upscale the refined image to final production quality."
|
|
4157
3950
|
}
|
|
4158
3951
|
],
|
|
4159
|
-
tags: [
|
|
4160
|
-
"refine",
|
|
4161
|
-
"iterate",
|
|
4162
|
-
"improve",
|
|
4163
|
-
"fix",
|
|
4164
|
-
"edit",
|
|
4165
|
-
"quality",
|
|
4166
|
-
"polish"
|
|
4167
|
-
]
|
|
3952
|
+
tags: ["refine", "iterate", "improve", "fix", "edit", "quality", "polish"]
|
|
4168
3953
|
},
|
|
4169
|
-
// --- Workflow ---
|
|
4170
3954
|
{
|
|
4171
3955
|
id: "hero-image-pipeline",
|
|
4172
3956
|
title: "Hero Image Pipeline",
|
|
@@ -4203,17 +3987,12 @@ var GUIDES = [
|
|
|
4203
3987
|
description: "Upscale to retina/4K resolution for high-DPI screens."
|
|
4204
3988
|
}
|
|
4205
3989
|
],
|
|
4206
|
-
tags: [
|
|
4207
|
-
"hero",
|
|
4208
|
-
"banner",
|
|
4209
|
-
"website",
|
|
4210
|
-
"marketing",
|
|
4211
|
-
"campaign",
|
|
4212
|
-
"landing-page",
|
|
4213
|
-
"header"
|
|
4214
|
-
]
|
|
3990
|
+
tags: ["hero", "banner", "website", "marketing", "campaign", "landing-page", "header"]
|
|
4215
3991
|
}
|
|
4216
3992
|
];
|
|
3993
|
+
|
|
3994
|
+
// src/core/guide-registry.ts
|
|
3995
|
+
var GUIDES = guides_default;
|
|
4217
3996
|
function getAllGuides() {
|
|
4218
3997
|
return GUIDES;
|
|
4219
3998
|
}
|
|
@@ -5726,532 +5505,9 @@ ${pc19.dim("Save as ref: gunni ref add <url> --name <name>")}
|
|
|
5726
5505
|
});
|
|
5727
5506
|
}
|
|
5728
5507
|
|
|
5729
|
-
// src/
|
|
5508
|
+
// src/ui/onboarding.ts
|
|
5730
5509
|
import * as clack3 from "@clack/prompts";
|
|
5731
5510
|
import pc20 from "picocolors";
|
|
5732
|
-
var DEFAULT_MODEL4 = "flux-2-pro";
|
|
5733
|
-
function registerGenerateCommand(program2) {
|
|
5734
|
-
program2.command("generate").description("Generate an image from a text prompt").argument("[prompt]", "Text prompt for image generation").option("-m, --model <model>", "Model to use (see: gunni list models --type image)", DEFAULT_MODEL4).option("-o, --output <path>", "Output file path (default: auto-generated)").option("--width <pixels>", "Image width in pixels", parseInt).option("--height <pixels>", "Image height in pixels", parseInt).option("--seed <number>", "Random seed for reproducibility", parseInt).addHelpText(
|
|
5735
|
-
"after",
|
|
5736
|
-
`
|
|
5737
|
-
Examples:
|
|
5738
|
-
|
|
5739
|
-
Interactive mode (guided walkthrough):
|
|
5740
|
-
$ gunni generate
|
|
5741
|
-
|
|
5742
|
-
Direct mode:
|
|
5743
|
-
$ gunni generate "a ceramic mug on a wooden table, morning light"
|
|
5744
|
-
$ gunni generate "a cat" --model flux-dev -o cat.png
|
|
5745
|
-
$ gunni generate "hero banner" --width 1920 --height 1080 -o hero.png
|
|
5746
|
-
$ gunni generate "abstract art" --seed 42 -o art.png
|
|
5747
|
-
|
|
5748
|
-
Agent usage (structured JSON output):
|
|
5749
|
-
$ gunni --json generate "product photo" --model flux-2-pro -o product.png
|
|
5750
|
-
|
|
5751
|
-
Returns: { images: [{ path, url, width, height }], seed, requestId, model, prompt }
|
|
5752
|
-
|
|
5753
|
-
Reproduce a previous generation:
|
|
5754
|
-
$ gunni generate "same prompt" --seed 12345 --model flux-2-pro -o v2.png
|
|
5755
|
-
|
|
5756
|
-
Available image models: flux-2-pro (default), flux-dev, flux-2-flex, grok-imagine, recraft-v4
|
|
5757
|
-
Run 'gunni list models --type image' for details.
|
|
5758
|
-
`
|
|
5759
|
-
).action(async (prompt, opts) => {
|
|
5760
|
-
const json = program2.opts().json ?? false;
|
|
5761
|
-
try {
|
|
5762
|
-
let finalPrompt;
|
|
5763
|
-
let finalModel;
|
|
5764
|
-
let finalOutput;
|
|
5765
|
-
let width = opts.width;
|
|
5766
|
-
let height = opts.height;
|
|
5767
|
-
let seed = opts.seed;
|
|
5768
|
-
if (prompt) {
|
|
5769
|
-
finalPrompt = prompt;
|
|
5770
|
-
finalModel = opts.model;
|
|
5771
|
-
finalOutput = opts.output ?? generateDefaultFilename(finalModel);
|
|
5772
|
-
} else {
|
|
5773
|
-
if (json) {
|
|
5774
|
-
throw new GunniError(
|
|
5775
|
-
"Prompt is required with --json",
|
|
5776
|
-
"MISSING_PROMPT"
|
|
5777
|
-
);
|
|
5778
|
-
}
|
|
5779
|
-
const interactive = await interactiveFlow(opts.model);
|
|
5780
|
-
finalPrompt = interactive.prompt;
|
|
5781
|
-
finalModel = interactive.model;
|
|
5782
|
-
finalOutput = interactive.output;
|
|
5783
|
-
width = interactive.width ?? width;
|
|
5784
|
-
height = interactive.height ?? height;
|
|
5785
|
-
seed = interactive.seed ?? seed;
|
|
5786
|
-
}
|
|
5787
|
-
const router = new ModelRouter();
|
|
5788
|
-
const result = await router.generate({
|
|
5789
|
-
prompt: finalPrompt,
|
|
5790
|
-
model: finalModel,
|
|
5791
|
-
outputPath: finalOutput,
|
|
5792
|
-
width,
|
|
5793
|
-
height,
|
|
5794
|
-
seed,
|
|
5795
|
-
showSpinner: !json
|
|
5796
|
-
});
|
|
5797
|
-
const img = result.images[0];
|
|
5798
|
-
logGeneration({
|
|
5799
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5800
|
-
command: "generate",
|
|
5801
|
-
model: result.model,
|
|
5802
|
-
prompt: result.prompt,
|
|
5803
|
-
outputPath: img.path,
|
|
5804
|
-
outputUrl: img.url,
|
|
5805
|
-
seed: result.seed,
|
|
5806
|
-
width: img.width,
|
|
5807
|
-
height: img.height,
|
|
5808
|
-
requestId: result.requestId
|
|
5809
|
-
}).catch(() => {
|
|
5810
|
-
});
|
|
5811
|
-
console.log(formatGenerateResult(result, json));
|
|
5812
|
-
} catch (err) {
|
|
5813
|
-
if (clack3.isCancel(err)) {
|
|
5814
|
-
clack3.cancel("Generation cancelled.");
|
|
5815
|
-
process.exit(0);
|
|
5816
|
-
}
|
|
5817
|
-
console.error(formatError(err, json));
|
|
5818
|
-
process.exit(1);
|
|
5819
|
-
}
|
|
5820
|
-
});
|
|
5821
|
-
}
|
|
5822
|
-
async function interactiveFlow(defaultModel) {
|
|
5823
|
-
clack3.intro(pc20.bold("gunni generate"));
|
|
5824
|
-
const prompt = await clack3.text({
|
|
5825
|
-
message: "What do you want to generate?",
|
|
5826
|
-
placeholder: "A ceramic mug on a wooden table, morning light",
|
|
5827
|
-
validate: (val) => {
|
|
5828
|
-
if (!val.trim()) return "Prompt cannot be empty";
|
|
5829
|
-
}
|
|
5830
|
-
});
|
|
5831
|
-
if (clack3.isCancel(prompt)) throw prompt;
|
|
5832
|
-
const { getModelsByCategory: getModelsByCategory2 } = await Promise.resolve().then(() => (init_model_registry(), model_registry_exports));
|
|
5833
|
-
const imageModels = getModelsByCategory2("image");
|
|
5834
|
-
const model = await clack3.select({
|
|
5835
|
-
message: "Which model?",
|
|
5836
|
-
options: imageModels.map((m) => ({
|
|
5837
|
-
value: m.id,
|
|
5838
|
-
label: m.name,
|
|
5839
|
-
hint: m.id === defaultModel ? "default" : m.description
|
|
5840
|
-
})),
|
|
5841
|
-
initialValue: defaultModel
|
|
5842
|
-
});
|
|
5843
|
-
if (clack3.isCancel(model)) throw model;
|
|
5844
|
-
let width;
|
|
5845
|
-
let height;
|
|
5846
|
-
let seed;
|
|
5847
|
-
const advanced = await clack3.confirm({
|
|
5848
|
-
message: "Configure advanced options?",
|
|
5849
|
-
initialValue: false
|
|
5850
|
-
});
|
|
5851
|
-
if (clack3.isCancel(advanced)) throw advanced;
|
|
5852
|
-
if (advanced) {
|
|
5853
|
-
const seedInput = await clack3.text({
|
|
5854
|
-
message: "Seed (leave blank for random):",
|
|
5855
|
-
placeholder: "42"
|
|
5856
|
-
});
|
|
5857
|
-
if (clack3.isCancel(seedInput)) throw seedInput;
|
|
5858
|
-
if (seedInput) seed = parseInt(seedInput, 10);
|
|
5859
|
-
const widthInput = await clack3.text({
|
|
5860
|
-
message: "Width in pixels (leave blank for default):",
|
|
5861
|
-
placeholder: "1024"
|
|
5862
|
-
});
|
|
5863
|
-
if (clack3.isCancel(widthInput)) throw widthInput;
|
|
5864
|
-
if (widthInput) width = parseInt(widthInput, 10);
|
|
5865
|
-
const heightInput = await clack3.text({
|
|
5866
|
-
message: "Height in pixels (leave blank for default):",
|
|
5867
|
-
placeholder: "1024"
|
|
5868
|
-
});
|
|
5869
|
-
if (clack3.isCancel(heightInput)) throw heightInput;
|
|
5870
|
-
if (heightInput) height = parseInt(heightInput, 10);
|
|
5871
|
-
}
|
|
5872
|
-
const output = await clack3.text({
|
|
5873
|
-
message: "Output file path:",
|
|
5874
|
-
defaultValue: generateDefaultFilename(model),
|
|
5875
|
-
placeholder: generateDefaultFilename(model)
|
|
5876
|
-
});
|
|
5877
|
-
if (clack3.isCancel(output)) throw output;
|
|
5878
|
-
clack3.log.step("Starting generation\u2026");
|
|
5879
|
-
return {
|
|
5880
|
-
prompt,
|
|
5881
|
-
model,
|
|
5882
|
-
output: output || generateDefaultFilename(model),
|
|
5883
|
-
width,
|
|
5884
|
-
height,
|
|
5885
|
-
seed
|
|
5886
|
-
};
|
|
5887
|
-
}
|
|
5888
|
-
function generateDefaultFilename(model) {
|
|
5889
|
-
const timestamp = Date.now();
|
|
5890
|
-
return `gunni-${model}-${timestamp}.png`;
|
|
5891
|
-
}
|
|
5892
|
-
|
|
5893
|
-
// src/commands/edit.ts
|
|
5894
|
-
import ora6 from "ora";
|
|
5895
|
-
init_model_registry();
|
|
5896
|
-
import pc21 from "picocolors";
|
|
5897
|
-
var DEFAULT_MODEL5 = "flux-kontext";
|
|
5898
|
-
function registerEditCommand(program2) {
|
|
5899
|
-
program2.command("edit").description("Edit an image with text instructions").argument("<image>", "Path to input image").argument("<prompt>", "Edit instructions").option("-m, --model <model>", "Model to use (see: gunni list models --type edit)", DEFAULT_MODEL5).option("-o, --output <path>", "Output file path (default: auto-generated)").addHelpText(
|
|
5900
|
-
"after",
|
|
5901
|
-
`
|
|
5902
|
-
Examples:
|
|
5903
|
-
|
|
5904
|
-
Basic editing:
|
|
5905
|
-
$ gunni edit photo.png "make the lighting warmer"
|
|
5906
|
-
$ gunni edit logo.png "place on a coffee cup mockup, studio lighting" -o mockup.png
|
|
5907
|
-
$ gunni edit hero.png "add subtle film grain and warmer tones" -o hero-v2.png
|
|
5908
|
-
|
|
5909
|
-
Chaining with generate:
|
|
5910
|
-
$ gunni generate "a ceramic mug on a table" -o mug.png
|
|
5911
|
-
$ gunni edit mug.png "add steam rising from the mug, morning light" -o mug-final.png
|
|
5912
|
-
|
|
5913
|
-
Agent usage (structured JSON output):
|
|
5914
|
-
$ gunni --json edit input.png "make it more vibrant" -o output.png
|
|
5915
|
-
|
|
5916
|
-
Returns: { image: { path, url, width, height }, model, prompt, inputImage }
|
|
5917
|
-
|
|
5918
|
-
Available edit models: flux-kontext (default), grok-edit
|
|
5919
|
-
Run 'gunni list models --type edit' for details.
|
|
5920
|
-
`
|
|
5921
|
-
).action(async (image, prompt, opts) => {
|
|
5922
|
-
const json = program2.opts().json ?? false;
|
|
5923
|
-
try {
|
|
5924
|
-
const spinner = json ? void 0 : ora6("Starting\u2026").start();
|
|
5925
|
-
const update = (s) => {
|
|
5926
|
-
if (spinner) spinner.text = s;
|
|
5927
|
-
};
|
|
5928
|
-
const imageUrl = await uploadToFal(image, update);
|
|
5929
|
-
update("Editing\u2026");
|
|
5930
|
-
const provider = new FalProvider();
|
|
5931
|
-
const result = await provider.call(
|
|
5932
|
-
getEndpoint2(opts.model ?? DEFAULT_MODEL5),
|
|
5933
|
-
{ image_url: imageUrl, prompt },
|
|
5934
|
-
update
|
|
5935
|
-
);
|
|
5936
|
-
const data = result.data;
|
|
5937
|
-
const img = data.images?.[0];
|
|
5938
|
-
if (!img) {
|
|
5939
|
-
spinner?.fail("No image returned");
|
|
5940
|
-
throw new GunniError("No image in response", "NO_IMAGE");
|
|
5941
|
-
}
|
|
5942
|
-
const requestedPath = opts.output ?? `gunni-edit-${Date.now()}.png`;
|
|
5943
|
-
const finalPath = await downloadImage(img.url, requestedPath, update);
|
|
5944
|
-
spinner?.succeed("Done");
|
|
5945
|
-
const output = {
|
|
5946
|
-
image: {
|
|
5947
|
-
path: finalPath,
|
|
5948
|
-
url: img.url,
|
|
5949
|
-
width: img.width ?? 0,
|
|
5950
|
-
height: img.height ?? 0
|
|
5951
|
-
},
|
|
5952
|
-
model: opts.model ?? DEFAULT_MODEL5,
|
|
5953
|
-
prompt,
|
|
5954
|
-
inputImage: image
|
|
5955
|
-
};
|
|
5956
|
-
logGeneration({
|
|
5957
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5958
|
-
command: "edit",
|
|
5959
|
-
model: output.model,
|
|
5960
|
-
prompt,
|
|
5961
|
-
inputImage: image,
|
|
5962
|
-
outputPath: finalPath,
|
|
5963
|
-
outputUrl: img.url,
|
|
5964
|
-
width: output.image.width,
|
|
5965
|
-
height: output.image.height
|
|
5966
|
-
}).catch(() => {
|
|
5967
|
-
});
|
|
5968
|
-
if (json) {
|
|
5969
|
-
console.log(JSON.stringify(output, null, 2));
|
|
5970
|
-
} else {
|
|
5971
|
-
console.log(`${pc21.green("\u2714")} Edited image saved to ${pc21.bold(finalPath)}`);
|
|
5972
|
-
console.log(` ${pc21.dim(`${output.image.width}\xD7${output.image.height} \xB7 ${output.model}`)}`);
|
|
5973
|
-
}
|
|
5974
|
-
} catch (err) {
|
|
5975
|
-
console.error(formatError(err, json));
|
|
5976
|
-
process.exit(1);
|
|
5977
|
-
}
|
|
5978
|
-
});
|
|
5979
|
-
}
|
|
5980
|
-
function getEndpoint2(modelId) {
|
|
5981
|
-
const model = getModel(modelId);
|
|
5982
|
-
if (!model) {
|
|
5983
|
-
throw new GunniError(
|
|
5984
|
-
`Unknown model: ${modelId}`,
|
|
5985
|
-
"UNKNOWN_MODEL",
|
|
5986
|
-
"Run `gunni list models --type edit` to see available models."
|
|
5987
|
-
);
|
|
5988
|
-
}
|
|
5989
|
-
return model.endpoint;
|
|
5990
|
-
}
|
|
5991
|
-
|
|
5992
|
-
// src/commands/describe.ts
|
|
5993
|
-
import ora7 from "ora";
|
|
5994
|
-
init_model_registry();
|
|
5995
|
-
var DEFAULT_MODEL6 = "florence-2";
|
|
5996
|
-
function registerDescribeCommand(program2) {
|
|
5997
|
-
program2.command("describe").description("Describe an image (image-to-text)").argument("<image>", "Path to image file").option("-m, --model <model>", "Model to use", DEFAULT_MODEL6).addHelpText(
|
|
5998
|
-
"after",
|
|
5999
|
-
`
|
|
6000
|
-
Examples:
|
|
6001
|
-
|
|
6002
|
-
Describe an image:
|
|
6003
|
-
$ gunni describe photo.png
|
|
6004
|
-
$ gunni describe logo.png --model florence-2
|
|
6005
|
-
|
|
6006
|
-
Agent usage \u2014 understand what was generated, then iterate:
|
|
6007
|
-
$ gunni generate "a logo for a coffee brand" -o logo.png
|
|
6008
|
-
$ gunni --json describe logo.png
|
|
6009
|
-
$ gunni edit logo.png "make the colors warmer" -o logo-v2.png
|
|
6010
|
-
|
|
6011
|
-
Structured JSON output:
|
|
6012
|
-
$ gunni --json describe image.png
|
|
6013
|
-
|
|
6014
|
-
Returns: { description, model, inputImage }
|
|
6015
|
-
`
|
|
6016
|
-
).action(async (image, opts) => {
|
|
6017
|
-
const json = program2.opts().json ?? false;
|
|
6018
|
-
try {
|
|
6019
|
-
const spinner = json ? void 0 : ora7("Starting\u2026").start();
|
|
6020
|
-
const update = (s) => {
|
|
6021
|
-
if (spinner) spinner.text = s;
|
|
6022
|
-
};
|
|
6023
|
-
const modelId = opts.model ?? DEFAULT_MODEL6;
|
|
6024
|
-
const modelDef = getModel(modelId);
|
|
6025
|
-
if (!modelDef) {
|
|
6026
|
-
spinner?.fail();
|
|
6027
|
-
throw new GunniError(
|
|
6028
|
-
`Unknown model: ${modelId}`,
|
|
6029
|
-
"UNKNOWN_MODEL",
|
|
6030
|
-
"Run `gunni list models --type describe` to see available models."
|
|
6031
|
-
);
|
|
6032
|
-
}
|
|
6033
|
-
const imageUrl = await uploadToFal(image, update);
|
|
6034
|
-
update("Analyzing\u2026");
|
|
6035
|
-
const provider = new FalProvider();
|
|
6036
|
-
const result = await provider.call(
|
|
6037
|
-
modelDef.endpoint,
|
|
6038
|
-
{ image_url: imageUrl },
|
|
6039
|
-
update
|
|
6040
|
-
);
|
|
6041
|
-
const data = result.data;
|
|
6042
|
-
const description = data.results ?? "";
|
|
6043
|
-
spinner?.succeed("Done");
|
|
6044
|
-
const output = {
|
|
6045
|
-
description,
|
|
6046
|
-
model: modelId,
|
|
6047
|
-
inputImage: image
|
|
6048
|
-
};
|
|
6049
|
-
logGeneration({
|
|
6050
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6051
|
-
command: "describe",
|
|
6052
|
-
model: modelId,
|
|
6053
|
-
inputImage: image,
|
|
6054
|
-
outputPath: image,
|
|
6055
|
-
metadata: { description }
|
|
6056
|
-
}).catch(() => {
|
|
6057
|
-
});
|
|
6058
|
-
if (json) {
|
|
6059
|
-
console.log(JSON.stringify(output, null, 2));
|
|
6060
|
-
} else {
|
|
6061
|
-
console.log(description);
|
|
6062
|
-
}
|
|
6063
|
-
} catch (err) {
|
|
6064
|
-
console.error(formatError(err, json));
|
|
6065
|
-
process.exit(1);
|
|
6066
|
-
}
|
|
6067
|
-
});
|
|
6068
|
-
}
|
|
6069
|
-
|
|
6070
|
-
// src/commands/upscale.ts
|
|
6071
|
-
import ora8 from "ora";
|
|
6072
|
-
init_model_registry();
|
|
6073
|
-
import pc22 from "picocolors";
|
|
6074
|
-
var DEFAULT_MODEL7 = "topaz-upscale";
|
|
6075
|
-
function registerUpscaleCommand(program2) {
|
|
6076
|
-
program2.command("upscale").description("Upscale an image to higher resolution").argument("<image>", "Path to input image").option("-o, --output <path>", "Output file path (default: auto-generated)").option("-s, --scale <factor>", "Scale factor (2 or 4)", parseInt, 2).option("-m, --model <model>", "Model to use", DEFAULT_MODEL7).addHelpText(
|
|
6077
|
-
"after",
|
|
6078
|
-
`
|
|
6079
|
-
Examples:
|
|
6080
|
-
|
|
6081
|
-
Upscale an image:
|
|
6082
|
-
$ gunni upscale photo.png -o photo-hires.png
|
|
6083
|
-
$ gunni upscale draft.png --scale 4 -o final.png
|
|
6084
|
-
|
|
6085
|
-
Chain with generate for production quality:
|
|
6086
|
-
$ gunni generate "product photo" -o draft.png
|
|
6087
|
-
$ gunni upscale draft.png -o production.png
|
|
6088
|
-
|
|
6089
|
-
Agent usage:
|
|
6090
|
-
$ gunni --json upscale image.png -o hires.png
|
|
6091
|
-
|
|
6092
|
-
Returns: { image: { path, url, width, height }, scale, model, inputImage }
|
|
6093
|
-
`
|
|
6094
|
-
).action(async (image, opts) => {
|
|
6095
|
-
const json = program2.opts().json ?? false;
|
|
6096
|
-
try {
|
|
6097
|
-
const spinner = json ? void 0 : ora8("Starting\u2026").start();
|
|
6098
|
-
const update = (s) => {
|
|
6099
|
-
if (spinner) spinner.text = s;
|
|
6100
|
-
};
|
|
6101
|
-
const modelId = opts.model ?? DEFAULT_MODEL7;
|
|
6102
|
-
const modelDef = getModel(modelId);
|
|
6103
|
-
if (!modelDef) {
|
|
6104
|
-
spinner?.fail();
|
|
6105
|
-
throw new GunniError(`Unknown model: ${modelId}`, "UNKNOWN_MODEL");
|
|
6106
|
-
}
|
|
6107
|
-
const imageUrl = await uploadToFal(image, update);
|
|
6108
|
-
update("Upscaling\u2026");
|
|
6109
|
-
const provider = new FalProvider();
|
|
6110
|
-
const result = await provider.call(
|
|
6111
|
-
modelDef.endpoint,
|
|
6112
|
-
{ image_url: imageUrl, scale: opts.scale },
|
|
6113
|
-
update
|
|
6114
|
-
);
|
|
6115
|
-
const data = result.data;
|
|
6116
|
-
const img = data.image;
|
|
6117
|
-
if (!img) {
|
|
6118
|
-
spinner?.fail("No image returned");
|
|
6119
|
-
throw new GunniError("No image in response", "NO_IMAGE");
|
|
6120
|
-
}
|
|
6121
|
-
const requestedPath = opts.output ?? `gunni-upscale-${Date.now()}.png`;
|
|
6122
|
-
const finalPath = await downloadImage(img.url, requestedPath, update);
|
|
6123
|
-
spinner?.succeed("Done");
|
|
6124
|
-
const output = {
|
|
6125
|
-
image: {
|
|
6126
|
-
path: finalPath,
|
|
6127
|
-
url: img.url,
|
|
6128
|
-
width: img.width ?? 0,
|
|
6129
|
-
height: img.height ?? 0
|
|
6130
|
-
},
|
|
6131
|
-
scale: opts.scale,
|
|
6132
|
-
model: modelId,
|
|
6133
|
-
inputImage: image
|
|
6134
|
-
};
|
|
6135
|
-
logGeneration({
|
|
6136
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6137
|
-
command: "upscale",
|
|
6138
|
-
model: modelId,
|
|
6139
|
-
inputImage: image,
|
|
6140
|
-
outputPath: finalPath,
|
|
6141
|
-
outputUrl: img.url,
|
|
6142
|
-
width: output.image.width,
|
|
6143
|
-
height: output.image.height,
|
|
6144
|
-
metadata: { scale: opts.scale }
|
|
6145
|
-
}).catch(() => {
|
|
6146
|
-
});
|
|
6147
|
-
if (json) {
|
|
6148
|
-
console.log(JSON.stringify(output, null, 2));
|
|
6149
|
-
} else {
|
|
6150
|
-
console.log(`${pc22.green("\u2714")} Upscaled image saved to ${pc22.bold(finalPath)}`);
|
|
6151
|
-
console.log(` ${pc22.dim(`${output.image.width}\xD7${output.image.height} \xB7 ${opts.scale}x \xB7 ${modelId}`)}`);
|
|
6152
|
-
}
|
|
6153
|
-
} catch (err) {
|
|
6154
|
-
console.error(formatError(err, json));
|
|
6155
|
-
process.exit(1);
|
|
6156
|
-
}
|
|
6157
|
-
});
|
|
6158
|
-
}
|
|
6159
|
-
|
|
6160
|
-
// src/commands/remove-bg.ts
|
|
6161
|
-
import ora9 from "ora";
|
|
6162
|
-
init_model_registry();
|
|
6163
|
-
import pc23 from "picocolors";
|
|
6164
|
-
var DEFAULT_MODEL8 = "bria-bg-remove";
|
|
6165
|
-
function registerRemoveBgCommand(program2) {
|
|
6166
|
-
program2.command("remove-bg").description("Remove background from an image").argument("<image>", "Path to input image").option("-o, --output <path>", "Output file path (default: auto-generated)").option("-m, --model <model>", "Model to use", DEFAULT_MODEL8).addHelpText(
|
|
6167
|
-
"after",
|
|
6168
|
-
`
|
|
6169
|
-
Examples:
|
|
6170
|
-
|
|
6171
|
-
Remove background:
|
|
6172
|
-
$ gunni remove-bg product.png -o product-clean.png
|
|
6173
|
-
$ gunni remove-bg photo.jpg -o cutout.png
|
|
6174
|
-
|
|
6175
|
-
Chain with other commands:
|
|
6176
|
-
$ gunni generate "a coffee bag product shot" -o bag.png
|
|
6177
|
-
$ gunni remove-bg bag.png -o bag-clean.png
|
|
6178
|
-
$ gunni edit bag-clean.png "place on a marble countertop" -o final.png
|
|
6179
|
-
|
|
6180
|
-
Agent usage:
|
|
6181
|
-
$ gunni --json remove-bg input.png -o output.png
|
|
6182
|
-
|
|
6183
|
-
Returns: { image: { path, url, width, height }, model, inputImage }
|
|
6184
|
-
`
|
|
6185
|
-
).action(async (image, opts) => {
|
|
6186
|
-
const json = program2.opts().json ?? false;
|
|
6187
|
-
try {
|
|
6188
|
-
const spinner = json ? void 0 : ora9("Starting\u2026").start();
|
|
6189
|
-
const update = (s) => {
|
|
6190
|
-
if (spinner) spinner.text = s;
|
|
6191
|
-
};
|
|
6192
|
-
const modelId = opts.model ?? DEFAULT_MODEL8;
|
|
6193
|
-
const modelDef = getModel(modelId);
|
|
6194
|
-
if (!modelDef) {
|
|
6195
|
-
spinner?.fail();
|
|
6196
|
-
throw new GunniError(`Unknown model: ${modelId}`, "UNKNOWN_MODEL");
|
|
6197
|
-
}
|
|
6198
|
-
const imageUrl = await uploadToFal(image, update);
|
|
6199
|
-
update("Removing background\u2026");
|
|
6200
|
-
const provider = new FalProvider();
|
|
6201
|
-
const result = await provider.call(
|
|
6202
|
-
modelDef.endpoint,
|
|
6203
|
-
{ image_url: imageUrl },
|
|
6204
|
-
update
|
|
6205
|
-
);
|
|
6206
|
-
const data = result.data;
|
|
6207
|
-
const img = data.image;
|
|
6208
|
-
if (!img) {
|
|
6209
|
-
spinner?.fail("No image returned");
|
|
6210
|
-
throw new GunniError("No image in response", "NO_IMAGE");
|
|
6211
|
-
}
|
|
6212
|
-
const requestedPath = opts.output ?? `gunni-nobg-${Date.now()}.png`;
|
|
6213
|
-
const finalPath = await downloadImage(img.url, requestedPath, update);
|
|
6214
|
-
spinner?.succeed("Done");
|
|
6215
|
-
const output = {
|
|
6216
|
-
image: {
|
|
6217
|
-
path: finalPath,
|
|
6218
|
-
url: img.url,
|
|
6219
|
-
width: img.width ?? 0,
|
|
6220
|
-
height: img.height ?? 0
|
|
6221
|
-
},
|
|
6222
|
-
model: modelId,
|
|
6223
|
-
inputImage: image
|
|
6224
|
-
};
|
|
6225
|
-
logGeneration({
|
|
6226
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6227
|
-
command: "remove-bg",
|
|
6228
|
-
model: modelId,
|
|
6229
|
-
inputImage: image,
|
|
6230
|
-
outputPath: finalPath,
|
|
6231
|
-
outputUrl: img.url,
|
|
6232
|
-
width: output.image.width,
|
|
6233
|
-
height: output.image.height
|
|
6234
|
-
}).catch(() => {
|
|
6235
|
-
});
|
|
6236
|
-
if (json) {
|
|
6237
|
-
console.log(JSON.stringify(output, null, 2));
|
|
6238
|
-
} else {
|
|
6239
|
-
console.log(`${pc23.green("\u2714")} Background removed, saved to ${pc23.bold(finalPath)}`);
|
|
6240
|
-
console.log(` ${pc23.dim(`${output.image.width}\xD7${output.image.height} \xB7 ${modelId}`)}`);
|
|
6241
|
-
}
|
|
6242
|
-
} catch (err) {
|
|
6243
|
-
console.error(formatError(err, json));
|
|
6244
|
-
process.exit(1);
|
|
6245
|
-
}
|
|
6246
|
-
});
|
|
6247
|
-
}
|
|
6248
|
-
|
|
6249
|
-
// src/index.ts
|
|
6250
|
-
init_model_registry();
|
|
6251
|
-
|
|
6252
|
-
// src/ui/onboarding.ts
|
|
6253
|
-
import * as clack4 from "@clack/prompts";
|
|
6254
|
-
import pc24 from "picocolors";
|
|
6255
5511
|
async function hasGunniKey() {
|
|
6256
5512
|
const config = new ConfigManager();
|
|
6257
5513
|
const key = await config.getGunniApiKey();
|
|
@@ -6259,47 +5515,47 @@ async function hasGunniKey() {
|
|
|
6259
5515
|
}
|
|
6260
5516
|
async function runOnboarding() {
|
|
6261
5517
|
console.log("");
|
|
6262
|
-
console.log(
|
|
5518
|
+
console.log(pc20.bold("Welcome to Gunni") + pc20.dim(" \u2014 AI media toolkit"));
|
|
6263
5519
|
console.log("");
|
|
6264
5520
|
console.log(" Generate images, video, and audio from your terminal.");
|
|
6265
5521
|
console.log(" One API key. 17 curated models. Zero config.");
|
|
6266
5522
|
console.log("");
|
|
6267
|
-
console.log(` Opening ${
|
|
5523
|
+
console.log(` Opening ${pc20.bold("gunni.ai/keys")} in your browser...`);
|
|
6268
5524
|
console.log(` Sign up (or log in), create an API key, and paste it below.`);
|
|
6269
5525
|
console.log("");
|
|
6270
5526
|
openUrl("https://gunni.ai/keys");
|
|
6271
|
-
const apiKey = await
|
|
5527
|
+
const apiKey = await clack3.text({
|
|
6272
5528
|
message: "Paste your Gunni API key:",
|
|
6273
5529
|
placeholder: "gn_live_...",
|
|
6274
5530
|
validate: (val) => {
|
|
6275
5531
|
if (!val.trim()) return "API key cannot be empty";
|
|
6276
5532
|
}
|
|
6277
5533
|
});
|
|
6278
|
-
if (
|
|
5534
|
+
if (clack3.isCancel(apiKey)) return false;
|
|
6279
5535
|
const config = new ConfigManager();
|
|
6280
5536
|
await config.setGunniApiKey(apiKey);
|
|
6281
5537
|
console.log("");
|
|
6282
|
-
console.log(` ${
|
|
5538
|
+
console.log(` ${pc20.green("\u2714")} API key saved. You're ready to go.`);
|
|
6283
5539
|
console.log("");
|
|
6284
|
-
console.log(` ${
|
|
6285
|
-
console.log(` ${
|
|
6286
|
-
console.log(` ${
|
|
6287
|
-
console.log(` ${
|
|
6288
|
-
console.log(` ${
|
|
6289
|
-
console.log(` ${
|
|
5540
|
+
console.log(` ${pc20.bold("Commands:")}`);
|
|
5541
|
+
console.log(` ${pc20.cyan("image")} Generate, edit, describe, upscale, remove-bg`);
|
|
5542
|
+
console.log(` ${pc20.cyan("video")} Image or text to video`);
|
|
5543
|
+
console.log(` ${pc20.cyan("audio")} Text to speech`);
|
|
5544
|
+
console.log(` ${pc20.cyan("learn")} Creative expertise and best practices`);
|
|
5545
|
+
console.log(` ${pc20.cyan("config")} Manage API keys`);
|
|
6290
5546
|
console.log("");
|
|
6291
|
-
console.log(` ${
|
|
6292
|
-
console.log(` ${
|
|
6293
|
-
console.log(` ${
|
|
6294
|
-
console.log(` ${
|
|
6295
|
-
console.log(` ${
|
|
5547
|
+
console.log(` ${pc20.bold("Quick start:")}`);
|
|
5548
|
+
console.log(` ${pc20.dim("$")} gunni image "coffee bag product shot" -o bag.png`);
|
|
5549
|
+
console.log(` ${pc20.dim("$")} gunni image bag.png --remove-bg -o bag-clean.png`);
|
|
5550
|
+
console.log(` ${pc20.dim("$")} gunni image bag-clean.png "on marble counter" -o hero.png`);
|
|
5551
|
+
console.log(` ${pc20.dim("$")} gunni video hero.png -p "slow cinematic zoom" -o hero.mp4`);
|
|
6296
5552
|
console.log("");
|
|
6297
5553
|
return true;
|
|
6298
5554
|
}
|
|
6299
5555
|
|
|
6300
5556
|
// src/index.ts
|
|
6301
5557
|
var program = new Command();
|
|
6302
|
-
program.name("gunni").description("AI media toolkit \u2014 give any agent the ability to create professional media").version("0.
|
|
5558
|
+
program.name("gunni").description("AI media toolkit \u2014 give any agent the ability to create professional media").version("0.3.3").option("--json", "Output results as JSON").addHelpText(
|
|
6303
5559
|
"after",
|
|
6304
5560
|
`
|
|
6305
5561
|
Examples:
|
|
@@ -6343,6 +5599,32 @@ registerInitCommand(program);
|
|
|
6343
5599
|
registerHistoryCommand(program);
|
|
6344
5600
|
registerConfigCommand(program);
|
|
6345
5601
|
registerListCommand(program);
|
|
5602
|
+
program.command("models").description("List available models (alias for: gunni list models)").option("-t, --type <category>", "Filter by category").action((opts) => {
|
|
5603
|
+
const json = program.opts().json ?? false;
|
|
5604
|
+
const models = opts.type ? getModelsByCategory(opts.type) : getAllModels();
|
|
5605
|
+
if (json) {
|
|
5606
|
+
console.log(JSON.stringify(models, null, 2));
|
|
5607
|
+
return;
|
|
5608
|
+
}
|
|
5609
|
+
if (models.length === 0) {
|
|
5610
|
+
console.log(`No models found${opts.type ? ` for category "${opts.type}"` : ""}.`);
|
|
5611
|
+
return;
|
|
5612
|
+
}
|
|
5613
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
5614
|
+
for (const model of models) {
|
|
5615
|
+
const list = grouped.get(model.category) ?? [];
|
|
5616
|
+
list.push(model);
|
|
5617
|
+
grouped.set(model.category, list);
|
|
5618
|
+
}
|
|
5619
|
+
for (const [category, catModels] of grouped) {
|
|
5620
|
+
console.log(`
|
|
5621
|
+
${category.toUpperCase()}`);
|
|
5622
|
+
for (const m of catModels) {
|
|
5623
|
+
console.log(` ${m.id} ${m.description}`);
|
|
5624
|
+
}
|
|
5625
|
+
}
|
|
5626
|
+
console.log();
|
|
5627
|
+
});
|
|
6346
5628
|
registerGuideCommand(program);
|
|
6347
5629
|
registerRefCommand(program);
|
|
6348
5630
|
registerStyleCommand(program);
|
|
@@ -6350,11 +5632,6 @@ registerTemplateCommand(program);
|
|
|
6350
5632
|
registerPresetCommand(program);
|
|
6351
5633
|
registerPipelineCommand(program);
|
|
6352
5634
|
registerResearchCommand(program);
|
|
6353
|
-
registerGenerateCommand(program);
|
|
6354
|
-
registerEditCommand(program);
|
|
6355
|
-
registerDescribeCommand(program);
|
|
6356
|
-
registerUpscaleCommand(program);
|
|
6357
|
-
registerRemoveBgCommand(program);
|
|
6358
5635
|
program.action(async () => {
|
|
6359
5636
|
const json = program.opts().json ?? false;
|
|
6360
5637
|
if (json) {
|
|
@@ -6371,7 +5648,7 @@ program.action(async () => {
|
|
|
6371
5648
|
{ name: "list models", usage: "gunni list models [--type <category>]", description: "List available models" },
|
|
6372
5649
|
{ name: "config", usage: "gunni config", description: "Manage API keys" }
|
|
6373
5650
|
];
|
|
6374
|
-
console.log(JSON.stringify({ version: "0.
|
|
5651
|
+
console.log(JSON.stringify({ version: "0.3.3", commands, categories, models }, null, 2));
|
|
6375
5652
|
return;
|
|
6376
5653
|
}
|
|
6377
5654
|
const configured = await hasGunniKey();
|
|
@@ -6381,49 +5658,49 @@ program.action(async () => {
|
|
|
6381
5658
|
return;
|
|
6382
5659
|
}
|
|
6383
5660
|
console.log(`
|
|
6384
|
-
${
|
|
6385
|
-
|
|
6386
|
-
${
|
|
6387
|
-
${
|
|
6388
|
-
${
|
|
6389
|
-
${
|
|
6390
|
-
|
|
6391
|
-
${
|
|
6392
|
-
${
|
|
6393
|
-
${
|
|
6394
|
-
${
|
|
6395
|
-
${
|
|
6396
|
-
|
|
6397
|
-
${
|
|
6398
|
-
${
|
|
6399
|
-
${
|
|
6400
|
-
|
|
6401
|
-
${
|
|
6402
|
-
${
|
|
6403
|
-
${
|
|
6404
|
-
${
|
|
6405
|
-
${
|
|
6406
|
-
|
|
6407
|
-
${
|
|
5661
|
+
${pc21.bold("GUNNI")} ${pc21.dim("v0.3.0")} \u2014 AI media toolkit
|
|
5662
|
+
|
|
5663
|
+
${pc21.bold("Media:")}
|
|
5664
|
+
${pc21.cyan("image")} Generate, edit, describe, upscale, remove-bg ${pc21.dim('gunni image "a cat" -o cat.png')}
|
|
5665
|
+
${pc21.cyan("video")} Image/text \u2192 video ${pc21.dim('gunni video hero.png -p "zoom in"')}
|
|
5666
|
+
${pc21.cyan("audio")} Text \u2192 speech ${pc21.dim('gunni audio "Hello" -o hello.mp3')}
|
|
5667
|
+
|
|
5668
|
+
${pc21.bold("Production:")}
|
|
5669
|
+
${pc21.cyan("template")} Browse & manage prompt templates ${pc21.dim("gunni template list --category ugc")}
|
|
5670
|
+
${pc21.cyan("preset")} Platform presets (framing, aspect ratio) ${pc21.dim("gunni preset get tiktok-ugc")}
|
|
5671
|
+
${pc21.cyan("pipeline")} Multi-step production workflows ${pc21.dim("gunni pipeline get ugc-product-reel")}
|
|
5672
|
+
${pc21.cyan("style")} Visual styles (brand identity) ${pc21.dim("gunni style list")}
|
|
5673
|
+
|
|
5674
|
+
${pc21.bold("Context:")}
|
|
5675
|
+
${pc21.cyan("learn")} Creative expertise & best practices ${pc21.dim("gunni learn exploration")}
|
|
5676
|
+
${pc21.cyan("latest")} New models, features, updates ${pc21.dim("gunni latest")}
|
|
5677
|
+
|
|
5678
|
+
${pc21.bold("Utility:")}
|
|
5679
|
+
${pc21.cyan("init")} Start a new project ${pc21.dim("gunni init --template brand")}
|
|
5680
|
+
${pc21.cyan("history")} Search past work ${pc21.dim('gunni history "coffee"')}
|
|
5681
|
+
${pc21.cyan("list")} Discover models ${pc21.dim("gunni list models")}
|
|
5682
|
+
${pc21.cyan("config")} API key setup ${pc21.dim("gunni config")}
|
|
5683
|
+
|
|
5684
|
+
${pc21.bold("Models:")} ${pc21.dim(`${getAllModels().length} curated models`)}`);
|
|
6408
5685
|
for (const cat of getCategories()) {
|
|
6409
5686
|
const models = getModelsByCategory(cat);
|
|
6410
5687
|
const defaultModel = models.find((m) => m.isDefault);
|
|
6411
5688
|
const others = models.filter((m) => m !== defaultModel);
|
|
6412
5689
|
if (defaultModel) {
|
|
6413
|
-
console.log(` ${
|
|
5690
|
+
console.log(` ${pc21.bold(cat.toUpperCase())} ${pc21.dim("\u2014")} ${pc21.cyan(defaultModel.id)}${others.length ? pc21.dim(`, ${others.map((m) => m.id).join(", ")}`) : ""}`);
|
|
6414
5691
|
} else {
|
|
6415
|
-
console.log(` ${
|
|
5692
|
+
console.log(` ${pc21.bold(cat.toUpperCase())} ${pc21.dim("\u2014")} ${models.map((m) => m.id).join(", ")}`);
|
|
6416
5693
|
}
|
|
6417
5694
|
}
|
|
6418
5695
|
console.log(`
|
|
6419
|
-
${
|
|
6420
|
-
${
|
|
6421
|
-
${
|
|
6422
|
-
${
|
|
6423
|
-
${
|
|
6424
|
-
|
|
6425
|
-
${
|
|
6426
|
-
${
|
|
5696
|
+
${pc21.bold("Quick start:")}
|
|
5697
|
+
${pc21.dim("$")} gunni image "coffee bag product shot" -o bag.png
|
|
5698
|
+
${pc21.dim("$")} gunni image bag.png --remove-bg -o bag-clean.png
|
|
5699
|
+
${pc21.dim("$")} gunni image bag-clean.png "on marble counter" -o hero.png
|
|
5700
|
+
${pc21.dim("$")} gunni image hero.png --upscale -o hero-final.png
|
|
5701
|
+
|
|
5702
|
+
${pc21.dim("Add --json to any command for structured output.")}
|
|
5703
|
+
${pc21.dim("Run gunni learn to load creative expertise.")}
|
|
6427
5704
|
`);
|
|
6428
5705
|
});
|
|
6429
5706
|
process.on("uncaughtException", (err) => {
|