@djangocfg/imgai 1.0.1
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/README.md +187 -0
- package/dist/cli/bin.cjs +996 -0
- package/dist/cli/bin.cjs.map +1 -0
- package/dist/cli/bin.d.cts +1 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +981 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/index.cjs +859 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +2 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +846 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index-DfR5DU9w.d.cts +221 -0
- package/dist/index-DfR5DU9w.d.ts +221 -0
- package/dist/index.cjs +905 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +100 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +882 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/cli/bin.js
ADDED
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import path2 from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import dotenv from 'dotenv';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import inquirer from 'inquirer';
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
import sharp from 'sharp';
|
|
11
|
+
import OpenAI from 'openai';
|
|
12
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
13
|
+
import { glob } from 'glob';
|
|
14
|
+
|
|
15
|
+
var ImageGenerator = class {
|
|
16
|
+
config;
|
|
17
|
+
openai;
|
|
18
|
+
anthropic;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.initializeProviders();
|
|
22
|
+
}
|
|
23
|
+
initializeProviders() {
|
|
24
|
+
const openaiKey = this.config.openaiApiKey || process.env.OPENAI_API_KEY;
|
|
25
|
+
const anthropicKey = this.config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
26
|
+
if (openaiKey) {
|
|
27
|
+
this.openai = new OpenAI({ apiKey: openaiKey });
|
|
28
|
+
}
|
|
29
|
+
if (anthropicKey) {
|
|
30
|
+
this.anthropic = new Anthropic({ apiKey: anthropicKey });
|
|
31
|
+
}
|
|
32
|
+
if (!this.openai && this.config.provider === "openai") {
|
|
33
|
+
throw new Error("OpenAI API key required. Set OPENAI_API_KEY or pass openaiApiKey in config.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
37
|
+
// SINGLE IMAGE GENERATION
|
|
38
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
39
|
+
async generate(options) {
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
try {
|
|
42
|
+
const fullPrompt = this.buildPrompt(options.prompt);
|
|
43
|
+
const imageData = await this.generateWithOpenAI(fullPrompt);
|
|
44
|
+
const filename = options.filename || this.generateFilename(options.prompt);
|
|
45
|
+
const category = options.category || "general";
|
|
46
|
+
const outputDir = path2.join(
|
|
47
|
+
this.config.projectRoot,
|
|
48
|
+
this.config.outputDir,
|
|
49
|
+
category
|
|
50
|
+
);
|
|
51
|
+
await fs.ensureDir(outputDir);
|
|
52
|
+
const originalPath = path2.join(outputDir, `${filename}.png`);
|
|
53
|
+
await fs.writeFile(originalPath, Buffer.from(imageData, "base64"));
|
|
54
|
+
const resizeOptions = options.resize || this.config.resize;
|
|
55
|
+
let finalPath = originalPath;
|
|
56
|
+
if (resizeOptions) {
|
|
57
|
+
finalPath = await this.resizeImage(originalPath, resizeOptions);
|
|
58
|
+
if (finalPath !== originalPath) {
|
|
59
|
+
await fs.remove(originalPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const imageInfo = await this.getImageInfo(finalPath, category);
|
|
63
|
+
imageInfo.metadata = {
|
|
64
|
+
prompt: options.prompt,
|
|
65
|
+
caption: options.metadata?.caption,
|
|
66
|
+
tags: options.tags,
|
|
67
|
+
category,
|
|
68
|
+
generatedAt: /* @__PURE__ */ new Date(),
|
|
69
|
+
model: "dall-e-3"
|
|
70
|
+
};
|
|
71
|
+
const duration = Date.now() - startTime;
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
imagePath: finalPath,
|
|
75
|
+
imageUrl: imageInfo.url,
|
|
76
|
+
imageInfo,
|
|
77
|
+
duration
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
83
|
+
duration: Date.now() - startTime
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
88
|
+
// BATCH GENERATION
|
|
89
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
90
|
+
async generateBatch(options) {
|
|
91
|
+
const startTime = Date.now();
|
|
92
|
+
const results = [];
|
|
93
|
+
const concurrency = options.concurrency || 2;
|
|
94
|
+
const delayBetween = options.delayBetween || 2e3;
|
|
95
|
+
for (let i = 0; i < options.items.length; i += concurrency) {
|
|
96
|
+
const batch = options.items.slice(i, i + concurrency);
|
|
97
|
+
const batchPromises = batch.map(async (item) => {
|
|
98
|
+
const result = await this.generate({
|
|
99
|
+
prompt: item.prompt,
|
|
100
|
+
filename: item.filename,
|
|
101
|
+
category: item.category
|
|
102
|
+
});
|
|
103
|
+
return result;
|
|
104
|
+
});
|
|
105
|
+
const batchResults = await Promise.all(batchPromises);
|
|
106
|
+
for (const result of batchResults) {
|
|
107
|
+
results.push(result);
|
|
108
|
+
if (options.onProgress) {
|
|
109
|
+
options.onProgress(results.length, options.items.length, result);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (i + concurrency < options.items.length) {
|
|
113
|
+
await this.delay(delayBetween);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
total: results.length,
|
|
118
|
+
success: results.filter((r) => r.success).length,
|
|
119
|
+
failed: results.filter((r) => !r.success).length,
|
|
120
|
+
results,
|
|
121
|
+
duration: Date.now() - startTime
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
125
|
+
// PROMPT ENHANCEMENT (using Claude/GPT)
|
|
126
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
127
|
+
async enhancePrompt(basePrompt, context) {
|
|
128
|
+
const systemPrompt = `You are an expert at creating image generation prompts.
|
|
129
|
+
Given a description, create:
|
|
130
|
+
1. A detailed visual prompt (max 150 words) for DALL-E 3
|
|
131
|
+
2. A descriptive filename (lowercase, hyphens, max 30 chars)
|
|
132
|
+
3. A short caption (max 50 words)
|
|
133
|
+
|
|
134
|
+
${this.config.style ? `Style guide: ${this.config.style}` : ""}
|
|
135
|
+
|
|
136
|
+
Return ONLY valid JSON: {"prompt": "...", "filename": "...", "caption": "..."}`;
|
|
137
|
+
const userPrompt = context ? `Description: ${basePrompt}
|
|
138
|
+
Context: ${context}` : `Description: ${basePrompt}`;
|
|
139
|
+
if (this.anthropic && this.config.provider === "anthropic") {
|
|
140
|
+
const response = await this.anthropic.messages.create({
|
|
141
|
+
model: "claude-3-5-sonnet-20241022",
|
|
142
|
+
max_tokens: 500,
|
|
143
|
+
messages: [
|
|
144
|
+
{ role: "user", content: `${systemPrompt}
|
|
145
|
+
|
|
146
|
+
${userPrompt}` }
|
|
147
|
+
]
|
|
148
|
+
});
|
|
149
|
+
const text = response.content[0].type === "text" ? response.content[0].text : "";
|
|
150
|
+
return JSON.parse(text);
|
|
151
|
+
}
|
|
152
|
+
if (this.openai) {
|
|
153
|
+
const response = await this.openai.chat.completions.create({
|
|
154
|
+
model: "gpt-4-turbo-preview",
|
|
155
|
+
messages: [
|
|
156
|
+
{ role: "system", content: systemPrompt },
|
|
157
|
+
{ role: "user", content: userPrompt }
|
|
158
|
+
],
|
|
159
|
+
max_tokens: 500,
|
|
160
|
+
temperature: 0.7,
|
|
161
|
+
response_format: { type: "json_object" }
|
|
162
|
+
});
|
|
163
|
+
const text = response.choices[0]?.message?.content || "{}";
|
|
164
|
+
return JSON.parse(text);
|
|
165
|
+
}
|
|
166
|
+
throw new Error("No AI provider configured for prompt enhancement");
|
|
167
|
+
}
|
|
168
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
169
|
+
// PRIVATE METHODS
|
|
170
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
171
|
+
async generateWithOpenAI(prompt) {
|
|
172
|
+
if (!this.openai) {
|
|
173
|
+
throw new Error("OpenAI client not initialized");
|
|
174
|
+
}
|
|
175
|
+
const response = await this.openai.images.generate({
|
|
176
|
+
model: "dall-e-3",
|
|
177
|
+
prompt,
|
|
178
|
+
n: 1,
|
|
179
|
+
size: this.config.size,
|
|
180
|
+
quality: this.config.quality,
|
|
181
|
+
response_format: "b64_json"
|
|
182
|
+
});
|
|
183
|
+
const imageData = response.data?.[0]?.b64_json;
|
|
184
|
+
if (!imageData) {
|
|
185
|
+
throw new Error("No image data received from OpenAI");
|
|
186
|
+
}
|
|
187
|
+
return imageData;
|
|
188
|
+
}
|
|
189
|
+
async resizeImage(inputPath, options) {
|
|
190
|
+
const { width, height, quality = 85, format = "webp", fit = "inside" } = options;
|
|
191
|
+
let pipeline = sharp(inputPath);
|
|
192
|
+
if (width || height) {
|
|
193
|
+
pipeline = pipeline.resize(width, height, {
|
|
194
|
+
fit,
|
|
195
|
+
withoutEnlargement: true
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const outputPath = inputPath.replace(/\.[^.]+$/, `.${format}`);
|
|
199
|
+
switch (format) {
|
|
200
|
+
case "webp":
|
|
201
|
+
pipeline = pipeline.webp({ quality });
|
|
202
|
+
break;
|
|
203
|
+
case "jpeg":
|
|
204
|
+
pipeline = pipeline.jpeg({ quality });
|
|
205
|
+
break;
|
|
206
|
+
case "png":
|
|
207
|
+
pipeline = pipeline.png({ quality });
|
|
208
|
+
break;
|
|
209
|
+
case "avif":
|
|
210
|
+
pipeline = pipeline.avif({ quality });
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
await pipeline.toFile(outputPath);
|
|
214
|
+
return outputPath;
|
|
215
|
+
}
|
|
216
|
+
async getImageInfo(imagePath, category) {
|
|
217
|
+
const stats = await fs.stat(imagePath);
|
|
218
|
+
const metadata = await sharp(imagePath).metadata();
|
|
219
|
+
const filename = path2.basename(imagePath);
|
|
220
|
+
const extension = path2.extname(filename).slice(1);
|
|
221
|
+
const id = path2.basename(filename, path2.extname(filename));
|
|
222
|
+
const publicDir = path2.join(this.config.projectRoot, this.config.publicDir);
|
|
223
|
+
const relativePath = path2.relative(publicDir, imagePath);
|
|
224
|
+
const url = "/" + relativePath.replace(/\\/g, "/");
|
|
225
|
+
return {
|
|
226
|
+
id,
|
|
227
|
+
filename,
|
|
228
|
+
extension,
|
|
229
|
+
path: relativePath,
|
|
230
|
+
url,
|
|
231
|
+
size: stats.size,
|
|
232
|
+
width: metadata.width,
|
|
233
|
+
height: metadata.height,
|
|
234
|
+
createdAt: stats.birthtime,
|
|
235
|
+
modifiedAt: stats.mtime
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
buildPrompt(basePrompt) {
|
|
239
|
+
if (this.config.style) {
|
|
240
|
+
return `${this.config.style}. ${basePrompt}`;
|
|
241
|
+
}
|
|
242
|
+
return basePrompt;
|
|
243
|
+
}
|
|
244
|
+
generateFilename(prompt) {
|
|
245
|
+
return prompt.toLowerCase().replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, "-").substring(0, 30);
|
|
246
|
+
}
|
|
247
|
+
delay(ms) {
|
|
248
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
var DEFAULT_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "avif", "gif", "svg"];
|
|
252
|
+
var DEFAULT_DIRECTORIES = ["static/images", "images", "assets"];
|
|
253
|
+
var ImageScanner = class {
|
|
254
|
+
config;
|
|
255
|
+
constructor(config) {
|
|
256
|
+
this.config = config;
|
|
257
|
+
}
|
|
258
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
259
|
+
// SCAN IMAGES
|
|
260
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
261
|
+
async scan(options = {}) {
|
|
262
|
+
const startTime = Date.now();
|
|
263
|
+
const {
|
|
264
|
+
directories = DEFAULT_DIRECTORIES,
|
|
265
|
+
extensions = DEFAULT_EXTENSIONS,
|
|
266
|
+
includeDimensions = true,
|
|
267
|
+
recursive = true
|
|
268
|
+
} = options;
|
|
269
|
+
const publicDir = path2.join(this.config.projectRoot, this.config.publicDir);
|
|
270
|
+
const images = [];
|
|
271
|
+
const byCategory = {};
|
|
272
|
+
const byExtension = {};
|
|
273
|
+
let totalSize = 0;
|
|
274
|
+
const patterns = directories.flatMap((dir) => {
|
|
275
|
+
const basePath = path2.join(publicDir, dir);
|
|
276
|
+
const extPattern = `*.{${extensions.join(",")}}`;
|
|
277
|
+
return recursive ? [path2.join(basePath, "**", extPattern)] : [path2.join(basePath, extPattern)];
|
|
278
|
+
});
|
|
279
|
+
for (const pattern of patterns) {
|
|
280
|
+
const files = await glob(pattern, { nodir: true });
|
|
281
|
+
for (const filePath of files) {
|
|
282
|
+
try {
|
|
283
|
+
const imageInfo = await this.processImage(filePath, publicDir, includeDimensions);
|
|
284
|
+
images.push(imageInfo);
|
|
285
|
+
totalSize += imageInfo.size;
|
|
286
|
+
byExtension[imageInfo.extension] = (byExtension[imageInfo.extension] || 0) + 1;
|
|
287
|
+
const category = this.extractCategory(imageInfo.path);
|
|
288
|
+
if (!byCategory[category]) {
|
|
289
|
+
byCategory[category] = [];
|
|
290
|
+
}
|
|
291
|
+
byCategory[category].push(imageInfo);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.warn(`Failed to process image: ${filePath}`, error);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
images.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
298
|
+
return {
|
|
299
|
+
images,
|
|
300
|
+
total: images.length,
|
|
301
|
+
byCategory,
|
|
302
|
+
byExtension,
|
|
303
|
+
totalSize,
|
|
304
|
+
scanDuration: Date.now() - startTime
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
308
|
+
// BUILD CATALOG
|
|
309
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
310
|
+
async buildCatalog(options = {}) {
|
|
311
|
+
const scanResult = await this.scan(options);
|
|
312
|
+
const byId = {};
|
|
313
|
+
for (const image of scanResult.images) {
|
|
314
|
+
byId[image.id] = image;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
version: "1.0.0",
|
|
318
|
+
generatedAt: /* @__PURE__ */ new Date(),
|
|
319
|
+
count: scanResult.total,
|
|
320
|
+
categories: scanResult.byCategory,
|
|
321
|
+
images: scanResult.images,
|
|
322
|
+
byId
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
326
|
+
// FIND SPECIFIC IMAGES
|
|
327
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
328
|
+
async findByCategory(category) {
|
|
329
|
+
const scanResult = await this.scan();
|
|
330
|
+
return scanResult.byCategory[category] || [];
|
|
331
|
+
}
|
|
332
|
+
async findById(id) {
|
|
333
|
+
const scanResult = await this.scan();
|
|
334
|
+
return scanResult.images.find((img) => img.id === id);
|
|
335
|
+
}
|
|
336
|
+
async findByExtension(extension) {
|
|
337
|
+
const scanResult = await this.scan();
|
|
338
|
+
return scanResult.images.filter((img) => img.extension === extension);
|
|
339
|
+
}
|
|
340
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
341
|
+
// PRIVATE METHODS
|
|
342
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
343
|
+
async processImage(filePath, publicDir, includeDimensions) {
|
|
344
|
+
const stats = await fs.stat(filePath);
|
|
345
|
+
const filename = path2.basename(filePath);
|
|
346
|
+
const extension = path2.extname(filename).slice(1).toLowerCase();
|
|
347
|
+
const id = path2.basename(filename, path2.extname(filename));
|
|
348
|
+
const relativePath = path2.relative(publicDir, filePath).replace(/\\/g, "/");
|
|
349
|
+
const url = "/" + relativePath;
|
|
350
|
+
let width;
|
|
351
|
+
let height;
|
|
352
|
+
if (includeDimensions && extension !== "svg") {
|
|
353
|
+
try {
|
|
354
|
+
const metadata = await sharp(filePath).metadata();
|
|
355
|
+
width = metadata.width;
|
|
356
|
+
height = metadata.height;
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
id,
|
|
362
|
+
filename,
|
|
363
|
+
extension,
|
|
364
|
+
path: relativePath,
|
|
365
|
+
url,
|
|
366
|
+
size: stats.size,
|
|
367
|
+
width,
|
|
368
|
+
height,
|
|
369
|
+
createdAt: stats.birthtime,
|
|
370
|
+
modifiedAt: stats.mtime
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
extractCategory(imagePath) {
|
|
374
|
+
const parts = imagePath.split("/");
|
|
375
|
+
if (parts.length > 1) {
|
|
376
|
+
return parts[parts.length - 2];
|
|
377
|
+
}
|
|
378
|
+
return "root";
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
function formatBytes(bytes) {
|
|
382
|
+
if (bytes === 0) return "0 B";
|
|
383
|
+
const k = 1024;
|
|
384
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
385
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
386
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
387
|
+
}
|
|
388
|
+
function formatDuration(ms) {
|
|
389
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
390
|
+
return `${(ms / 1e3).toFixed(2)}s`;
|
|
391
|
+
}
|
|
392
|
+
var ConfigGenerator = class {
|
|
393
|
+
config;
|
|
394
|
+
scanner;
|
|
395
|
+
constructor(config) {
|
|
396
|
+
this.config = config;
|
|
397
|
+
this.scanner = new ImageScanner(config);
|
|
398
|
+
}
|
|
399
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
400
|
+
// GENERATE IMAGES.TS
|
|
401
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
402
|
+
async generate() {
|
|
403
|
+
const catalog = await this.scanner.buildCatalog();
|
|
404
|
+
const outputPath = path2.join(this.config.projectRoot, this.config.configOutputPath);
|
|
405
|
+
await fs.ensureDir(path2.dirname(outputPath));
|
|
406
|
+
const content = this.generateTypeScript(catalog);
|
|
407
|
+
await fs.writeFile(outputPath, content, "utf-8");
|
|
408
|
+
return outputPath;
|
|
409
|
+
}
|
|
410
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
411
|
+
// TYPESCRIPT GENERATION
|
|
412
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
413
|
+
generateTypeScript(catalog) {
|
|
414
|
+
const categories = Object.keys(catalog.categories).sort();
|
|
415
|
+
const allIds = catalog.images.map((img) => img.id).sort();
|
|
416
|
+
return `/**
|
|
417
|
+
* Auto-generated image catalog
|
|
418
|
+
* Generated: ${catalog.generatedAt.toISOString()}
|
|
419
|
+
* Total images: ${catalog.count}
|
|
420
|
+
*
|
|
421
|
+
* DO NOT EDIT - This file is auto-generated by @djangocfg/imgai
|
|
422
|
+
* Run \`imgai sync\` to regenerate
|
|
423
|
+
*/
|
|
424
|
+
|
|
425
|
+
// ============================================================================
|
|
426
|
+
// TYPES
|
|
427
|
+
// ============================================================================
|
|
428
|
+
|
|
429
|
+
export interface ImageData {
|
|
430
|
+
id: string;
|
|
431
|
+
filename: string;
|
|
432
|
+
url: string;
|
|
433
|
+
path: string;
|
|
434
|
+
extension: string;
|
|
435
|
+
width?: number;
|
|
436
|
+
height?: number;
|
|
437
|
+
size: number;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export type ImageId = ${allIds.length > 0 ? allIds.map((id) => `'${id}'`).join(" | ") : "string"};
|
|
441
|
+
export type ImageCategory = ${categories.length > 0 ? categories.map((c) => `'${c}'`).join(" | ") : "string"};
|
|
442
|
+
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// IMAGE CATALOG
|
|
445
|
+
// ============================================================================
|
|
446
|
+
|
|
447
|
+
export const IMAGES: Record<ImageId, ImageData> = {
|
|
448
|
+
${catalog.images.map((img) => this.generateImageEntry(img)).join(",\n")}
|
|
449
|
+
} as const;
|
|
450
|
+
|
|
451
|
+
// ============================================================================
|
|
452
|
+
// BY CATEGORY
|
|
453
|
+
// ============================================================================
|
|
454
|
+
|
|
455
|
+
export const IMAGES_BY_CATEGORY: Record<ImageCategory, readonly ImageData[]> = {
|
|
456
|
+
${categories.map((cat) => this.generateCategoryEntry(cat, catalog.categories[cat] || [])).join(",\n")}
|
|
457
|
+
} as const;
|
|
458
|
+
|
|
459
|
+
// ============================================================================
|
|
460
|
+
// HELPERS
|
|
461
|
+
// ============================================================================
|
|
462
|
+
|
|
463
|
+
/** Get image by ID */
|
|
464
|
+
export function getImage(id: ImageId): ImageData | undefined {
|
|
465
|
+
return IMAGES[id];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Get image URL by ID */
|
|
469
|
+
export function getImageUrl(id: ImageId): string | undefined {
|
|
470
|
+
return IMAGES[id]?.url;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** Get all images in category */
|
|
474
|
+
export function getImagesByCategory(category: ImageCategory): readonly ImageData[] {
|
|
475
|
+
return IMAGES_BY_CATEGORY[category] || [];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Get all image IDs */
|
|
479
|
+
export function getAllImageIds(): ImageId[] {
|
|
480
|
+
return Object.keys(IMAGES) as ImageId[];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Get all categories */
|
|
484
|
+
export function getAllCategories(): ImageCategory[] {
|
|
485
|
+
return Object.keys(IMAGES_BY_CATEGORY) as ImageCategory[];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Check if image exists */
|
|
489
|
+
export function hasImage(id: string): id is ImageId {
|
|
490
|
+
return id in IMAGES;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ============================================================================
|
|
494
|
+
// METADATA
|
|
495
|
+
// ============================================================================
|
|
496
|
+
|
|
497
|
+
export const IMAGE_CATALOG_META = {
|
|
498
|
+
version: '${catalog.version}',
|
|
499
|
+
generatedAt: '${catalog.generatedAt.toISOString()}',
|
|
500
|
+
totalImages: ${catalog.count},
|
|
501
|
+
categories: ${JSON.stringify(categories)},
|
|
502
|
+
} as const;
|
|
503
|
+
`;
|
|
504
|
+
}
|
|
505
|
+
generateImageEntry(img) {
|
|
506
|
+
const data = {
|
|
507
|
+
id: img.id,
|
|
508
|
+
filename: img.filename,
|
|
509
|
+
url: img.url,
|
|
510
|
+
path: img.path,
|
|
511
|
+
extension: img.extension,
|
|
512
|
+
size: img.size
|
|
513
|
+
};
|
|
514
|
+
if (img.width) data.width = img.width;
|
|
515
|
+
if (img.height) data.height = img.height;
|
|
516
|
+
return ` '${img.id}': ${JSON.stringify(data, null, 4).replace(/\n/g, "\n ").replace(/}$/, " }")}`;
|
|
517
|
+
}
|
|
518
|
+
generateCategoryEntry(category, images) {
|
|
519
|
+
const imageRefs = images.map((img) => `IMAGES['${img.id}']`);
|
|
520
|
+
return ` '${category}': [${imageRefs.join(", ")}]`;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// src/cli/index.ts
|
|
525
|
+
var ImageAICLI = class {
|
|
526
|
+
config;
|
|
527
|
+
generator;
|
|
528
|
+
scanner;
|
|
529
|
+
configGenerator;
|
|
530
|
+
constructor(config) {
|
|
531
|
+
this.config = config;
|
|
532
|
+
this.generator = new ImageGenerator(config);
|
|
533
|
+
this.scanner = new ImageScanner(config);
|
|
534
|
+
this.configGenerator = new ConfigGenerator(config);
|
|
535
|
+
}
|
|
536
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
537
|
+
// INTERACTIVE MODE
|
|
538
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
539
|
+
async interactive() {
|
|
540
|
+
this.printHeader();
|
|
541
|
+
while (true) {
|
|
542
|
+
try {
|
|
543
|
+
const { action } = await inquirer.prompt([
|
|
544
|
+
{
|
|
545
|
+
type: "list",
|
|
546
|
+
name: "action",
|
|
547
|
+
message: chalk.cyan("What would you like to do?"),
|
|
548
|
+
choices: [
|
|
549
|
+
{ name: `${chalk.green("\u{1F3A8}")} Generate image from prompt`, value: "generate" },
|
|
550
|
+
{ name: `${chalk.blue("\u2728")} Generate with AI-enhanced prompt`, value: "enhance" },
|
|
551
|
+
{ name: `${chalk.yellow("\u{1F4E6}")} Batch generate images`, value: "batch" },
|
|
552
|
+
new inquirer.Separator(),
|
|
553
|
+
{ name: `${chalk.magenta("\u{1F50D}")} Scan & catalog images`, value: "scan" },
|
|
554
|
+
{ name: `${chalk.cyan("\u26A1")} Sync images.ts config`, value: "sync" },
|
|
555
|
+
{ name: `${chalk.white("\u{1F4CA}")} Show statistics`, value: "stats" },
|
|
556
|
+
new inquirer.Separator(),
|
|
557
|
+
{ name: `${chalk.gray("\u2699\uFE0F")} Settings`, value: "settings" },
|
|
558
|
+
{ name: `${chalk.red("\u{1F6AA}")} Exit`, value: "exit" }
|
|
559
|
+
]
|
|
560
|
+
}
|
|
561
|
+
]);
|
|
562
|
+
switch (action) {
|
|
563
|
+
case "generate":
|
|
564
|
+
await this.promptGenerate();
|
|
565
|
+
break;
|
|
566
|
+
case "enhance":
|
|
567
|
+
await this.promptEnhance();
|
|
568
|
+
break;
|
|
569
|
+
case "batch":
|
|
570
|
+
await this.promptBatch();
|
|
571
|
+
break;
|
|
572
|
+
case "scan":
|
|
573
|
+
await this.runScan();
|
|
574
|
+
break;
|
|
575
|
+
case "sync":
|
|
576
|
+
await this.runSync();
|
|
577
|
+
break;
|
|
578
|
+
case "stats":
|
|
579
|
+
await this.showStats();
|
|
580
|
+
break;
|
|
581
|
+
case "settings":
|
|
582
|
+
await this.showSettings();
|
|
583
|
+
break;
|
|
584
|
+
case "exit":
|
|
585
|
+
console.log(chalk.cyan("\n\u{1F44B} Goodbye!\n"));
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
console.log("");
|
|
589
|
+
} catch (error) {
|
|
590
|
+
if (error instanceof Error && error.name === "ExitPromptError") {
|
|
591
|
+
console.log(chalk.cyan("\n\n\u{1F44B} Goodbye!\n"));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
throw error;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
599
|
+
// GENERATE COMMANDS
|
|
600
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
601
|
+
async generate(options) {
|
|
602
|
+
const spinner = ora("Generating image...").start();
|
|
603
|
+
try {
|
|
604
|
+
const result = await this.generator.generate(options);
|
|
605
|
+
if (result.success) {
|
|
606
|
+
spinner.succeed(chalk.green("Image generated successfully!"));
|
|
607
|
+
console.log(chalk.gray(` Path: ${result.imagePath}`));
|
|
608
|
+
console.log(chalk.gray(` URL: ${result.imageUrl}`));
|
|
609
|
+
console.log(chalk.gray(` Time: ${formatDuration(result.duration || 0)}`));
|
|
610
|
+
} else {
|
|
611
|
+
spinner.fail(chalk.red(`Generation failed: ${result.error}`));
|
|
612
|
+
}
|
|
613
|
+
} catch (error) {
|
|
614
|
+
spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async scan(options = {}) {
|
|
618
|
+
const spinner = ora("Scanning images...").start();
|
|
619
|
+
try {
|
|
620
|
+
const result = await this.scanner.scan(options);
|
|
621
|
+
spinner.succeed(chalk.green(`Found ${result.total} images`));
|
|
622
|
+
console.log(chalk.gray(` Total size: ${formatBytes(result.totalSize)}`));
|
|
623
|
+
console.log(chalk.gray(` Scan time: ${formatDuration(result.scanDuration)}`));
|
|
624
|
+
console.log(chalk.cyan("\n Categories:"));
|
|
625
|
+
for (const [category, images] of Object.entries(result.byCategory)) {
|
|
626
|
+
console.log(chalk.gray(` ${category}: ${images.length} images`));
|
|
627
|
+
}
|
|
628
|
+
console.log(chalk.cyan("\n Extensions:"));
|
|
629
|
+
for (const [ext, count] of Object.entries(result.byExtension)) {
|
|
630
|
+
console.log(chalk.gray(` .${ext}: ${count} files`));
|
|
631
|
+
}
|
|
632
|
+
} catch (error) {
|
|
633
|
+
spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async sync() {
|
|
637
|
+
const spinner = ora("Syncing images.ts config...").start();
|
|
638
|
+
try {
|
|
639
|
+
const outputPath = await this.configGenerator.generate();
|
|
640
|
+
spinner.succeed(chalk.green("Config synced successfully!"));
|
|
641
|
+
console.log(chalk.gray(` Output: ${outputPath}`));
|
|
642
|
+
} catch (error) {
|
|
643
|
+
spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
647
|
+
// PRIVATE PROMPT METHODS
|
|
648
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
649
|
+
async promptGenerate() {
|
|
650
|
+
const answers = await inquirer.prompt([
|
|
651
|
+
{
|
|
652
|
+
type: "input",
|
|
653
|
+
name: "prompt",
|
|
654
|
+
message: "Enter image description:",
|
|
655
|
+
validate: (input) => input.trim() ? true : "Description is required"
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
type: "input",
|
|
659
|
+
name: "filename",
|
|
660
|
+
message: "Filename (optional, auto-generated if empty):"
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
type: "input",
|
|
664
|
+
name: "category",
|
|
665
|
+
message: "Category/folder (default: general):",
|
|
666
|
+
default: "general"
|
|
667
|
+
}
|
|
668
|
+
]);
|
|
669
|
+
await this.generate({
|
|
670
|
+
prompt: answers.prompt,
|
|
671
|
+
filename: answers.filename || void 0,
|
|
672
|
+
category: answers.category
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
async promptEnhance() {
|
|
676
|
+
const answers = await inquirer.prompt([
|
|
677
|
+
{
|
|
678
|
+
type: "input",
|
|
679
|
+
name: "description",
|
|
680
|
+
message: "Describe what you want:",
|
|
681
|
+
validate: (input) => input.trim() ? true : "Description is required"
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
type: "input",
|
|
685
|
+
name: "context",
|
|
686
|
+
message: "Additional context (optional):"
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
type: "input",
|
|
690
|
+
name: "category",
|
|
691
|
+
message: "Category/folder (default: general):",
|
|
692
|
+
default: "general"
|
|
693
|
+
}
|
|
694
|
+
]);
|
|
695
|
+
const spinner = ora("Enhancing prompt with AI...").start();
|
|
696
|
+
try {
|
|
697
|
+
const enhanced = await this.generator.enhancePrompt(
|
|
698
|
+
answers.description,
|
|
699
|
+
answers.context || void 0
|
|
700
|
+
);
|
|
701
|
+
spinner.succeed("Prompt enhanced!");
|
|
702
|
+
console.log(chalk.cyan("\n Enhanced prompt:"));
|
|
703
|
+
console.log(chalk.gray(` ${enhanced.prompt}
|
|
704
|
+
`));
|
|
705
|
+
console.log(chalk.gray(` Filename: ${enhanced.filename}`));
|
|
706
|
+
console.log(chalk.gray(` Caption: ${enhanced.caption}
|
|
707
|
+
`));
|
|
708
|
+
const { confirm } = await inquirer.prompt([
|
|
709
|
+
{
|
|
710
|
+
type: "confirm",
|
|
711
|
+
name: "confirm",
|
|
712
|
+
message: "Generate image with this prompt?",
|
|
713
|
+
default: true
|
|
714
|
+
}
|
|
715
|
+
]);
|
|
716
|
+
if (confirm) {
|
|
717
|
+
await this.generate({
|
|
718
|
+
prompt: enhanced.prompt,
|
|
719
|
+
filename: enhanced.filename,
|
|
720
|
+
category: answers.category,
|
|
721
|
+
metadata: { caption: enhanced.caption }
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
} catch (error) {
|
|
725
|
+
spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async promptBatch() {
|
|
729
|
+
console.log(chalk.cyan("\n Enter prompts (one per line, empty line to finish):"));
|
|
730
|
+
const prompts = [];
|
|
731
|
+
let lineNumber = 1;
|
|
732
|
+
while (true) {
|
|
733
|
+
const { prompt } = await inquirer.prompt([
|
|
734
|
+
{
|
|
735
|
+
type: "input",
|
|
736
|
+
name: "prompt",
|
|
737
|
+
message: ` ${lineNumber}.`
|
|
738
|
+
}
|
|
739
|
+
]);
|
|
740
|
+
if (!prompt.trim()) break;
|
|
741
|
+
prompts.push(prompt);
|
|
742
|
+
lineNumber++;
|
|
743
|
+
}
|
|
744
|
+
if (prompts.length === 0) {
|
|
745
|
+
console.log(chalk.yellow(" No prompts entered."));
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const { category, concurrency } = await inquirer.prompt([
|
|
749
|
+
{
|
|
750
|
+
type: "input",
|
|
751
|
+
name: "category",
|
|
752
|
+
message: "Category for all images:",
|
|
753
|
+
default: "batch"
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
type: "list",
|
|
757
|
+
name: "concurrency",
|
|
758
|
+
message: "Concurrent workers:",
|
|
759
|
+
choices: [
|
|
760
|
+
{ name: "1 (slow, safe)", value: 1 },
|
|
761
|
+
{ name: "2 (balanced)", value: 2 },
|
|
762
|
+
{ name: "3 (fast)", value: 3 }
|
|
763
|
+
],
|
|
764
|
+
default: 2
|
|
765
|
+
}
|
|
766
|
+
]);
|
|
767
|
+
console.log(chalk.cyan(`
|
|
768
|
+
Generating ${prompts.length} images...
|
|
769
|
+
`));
|
|
770
|
+
const result = await this.generator.generateBatch({
|
|
771
|
+
items: prompts.map((prompt, i) => ({
|
|
772
|
+
prompt,
|
|
773
|
+
category,
|
|
774
|
+
filename: `batch-${Date.now()}-${i + 1}`
|
|
775
|
+
})),
|
|
776
|
+
concurrency,
|
|
777
|
+
onProgress: (current, total, res) => {
|
|
778
|
+
const status = res.success ? chalk.green("\u2713") : chalk.red("\u2717");
|
|
779
|
+
console.log(` ${status} ${current}/${total}`);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
console.log(chalk.cyan("\n Batch complete!"));
|
|
783
|
+
console.log(chalk.gray(` Success: ${result.success}`));
|
|
784
|
+
console.log(chalk.gray(` Failed: ${result.failed}`));
|
|
785
|
+
console.log(chalk.gray(` Time: ${formatDuration(result.duration)}`));
|
|
786
|
+
}
|
|
787
|
+
async runScan() {
|
|
788
|
+
await this.scan();
|
|
789
|
+
}
|
|
790
|
+
async runSync() {
|
|
791
|
+
await this.sync();
|
|
792
|
+
}
|
|
793
|
+
async showStats() {
|
|
794
|
+
const spinner = ora("Gathering statistics...").start();
|
|
795
|
+
try {
|
|
796
|
+
const result = await this.scanner.scan();
|
|
797
|
+
spinner.stop();
|
|
798
|
+
console.log(chalk.cyan("\n \u{1F4CA} Image Statistics\n"));
|
|
799
|
+
console.log(chalk.white(` Total images: ${result.total}`));
|
|
800
|
+
console.log(chalk.white(` Total size: ${formatBytes(result.totalSize)}`));
|
|
801
|
+
console.log(chalk.white(` Categories: ${Object.keys(result.byCategory).length}`));
|
|
802
|
+
console.log(chalk.cyan("\n By Category:"));
|
|
803
|
+
for (const [category, images] of Object.entries(result.byCategory).sort((a, b) => b[1].length - a[1].length)) {
|
|
804
|
+
const size = images.reduce((sum, img) => sum + img.size, 0);
|
|
805
|
+
console.log(chalk.gray(` ${category.padEnd(20)} ${images.length.toString().padStart(4)} images ${formatBytes(size).padStart(10)}`));
|
|
806
|
+
}
|
|
807
|
+
console.log(chalk.cyan("\n By Extension:"));
|
|
808
|
+
for (const [ext, count] of Object.entries(result.byExtension).sort((a, b) => b[1] - a[1])) {
|
|
809
|
+
console.log(chalk.gray(` .${ext.padEnd(6)} ${count} files`));
|
|
810
|
+
}
|
|
811
|
+
} catch (error) {
|
|
812
|
+
spinner.fail(chalk.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async showSettings() {
|
|
816
|
+
console.log(chalk.cyan("\n \u2699\uFE0F Current Settings\n"));
|
|
817
|
+
console.log(chalk.gray(` Provider: ${this.config.provider}`));
|
|
818
|
+
console.log(chalk.gray(` Size: ${this.config.size}`));
|
|
819
|
+
console.log(chalk.gray(` Quality: ${this.config.quality}`));
|
|
820
|
+
console.log(chalk.gray(` Output dir: ${this.config.outputDir}`));
|
|
821
|
+
console.log(chalk.gray(` Config path: ${this.config.configOutputPath}`));
|
|
822
|
+
if (this.config.resize) {
|
|
823
|
+
console.log(chalk.cyan("\n Resize Settings:"));
|
|
824
|
+
console.log(chalk.gray(` Width: ${this.config.resize.width || "auto"}`));
|
|
825
|
+
console.log(chalk.gray(` Height: ${this.config.resize.height || "auto"}`));
|
|
826
|
+
console.log(chalk.gray(` Format: ${this.config.resize.format}`));
|
|
827
|
+
console.log(chalk.gray(` Quality: ${this.config.resize.quality}`));
|
|
828
|
+
}
|
|
829
|
+
if (this.config.style) {
|
|
830
|
+
console.log(chalk.cyan("\n Style:"));
|
|
831
|
+
console.log(chalk.gray(` ${this.config.style.substring(0, 100)}...`));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
835
|
+
// UI HELPERS
|
|
836
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
837
|
+
printHeader() {
|
|
838
|
+
console.log("");
|
|
839
|
+
console.log(chalk.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
840
|
+
console.log(chalk.cyan(" \u2551") + chalk.white.bold(" \u{1F3A8} IMGAI - Image Generator ") + chalk.cyan("\u2551"));
|
|
841
|
+
console.log(chalk.cyan(" \u2551") + chalk.gray(" AI-powered image generation & sync ") + chalk.cyan("\u2551"));
|
|
842
|
+
console.log(chalk.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
843
|
+
console.log("");
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
// src/cli/bin.ts
|
|
848
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
849
|
+
var __dirname = path2.dirname(__filename);
|
|
850
|
+
dotenv.config();
|
|
851
|
+
dotenv.config({ path: path2.resolve(process.cwd(), ".env.local") });
|
|
852
|
+
dotenv.config({ path: path2.resolve(__dirname, "../../.env") });
|
|
853
|
+
var program = new Command();
|
|
854
|
+
function createConfig(options) {
|
|
855
|
+
const projectRoot = options.root || process.cwd();
|
|
856
|
+
return {
|
|
857
|
+
provider: options.provider || "openai",
|
|
858
|
+
openaiApiKey: process.env.OPENAI_API_KEY,
|
|
859
|
+
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
|
860
|
+
size: options.size || "1792x1024",
|
|
861
|
+
quality: options.quality || "standard",
|
|
862
|
+
style: options.style,
|
|
863
|
+
outputDir: options.output || "public/static/images",
|
|
864
|
+
projectRoot,
|
|
865
|
+
srcDir: "src",
|
|
866
|
+
publicDir: "public",
|
|
867
|
+
configOutputPath: options.config || "src/core/images.ts",
|
|
868
|
+
resize: options.resize ? {
|
|
869
|
+
width: options.width,
|
|
870
|
+
height: options.height,
|
|
871
|
+
quality: options.quality || 85,
|
|
872
|
+
format: options.format || "webp",
|
|
873
|
+
fit: "inside"
|
|
874
|
+
} : void 0
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
program.name("imgai").description("AI-powered image generation & management for Next.js").version("1.0.0");
|
|
878
|
+
program.command("interactive", { isDefault: true }).alias("i").description("Start interactive mode").option("-r, --root <path>", "Project root directory").action(async (options) => {
|
|
879
|
+
const config = createConfig(options);
|
|
880
|
+
const cli = new ImageAICLI(config);
|
|
881
|
+
await cli.interactive();
|
|
882
|
+
});
|
|
883
|
+
program.command("generate <prompt>").alias("g").description("Generate image from prompt").option("-f, --filename <name>", "Output filename").option("-c, --category <name>", "Category/folder", "general").option("-r, --root <path>", "Project root directory").option("-o, --output <path>", "Output directory", "public/static/images").option("--size <size>", "Image size (1024x1024, 1792x1024, 1024x1792)", "1792x1024").option("--quality <quality>", "Image quality (standard, hd)", "standard").option("--style <style>", "Style prefix for prompts").option("--resize", "Enable resize").option("--width <pixels>", "Resize width", parseInt).option("--height <pixels>", "Resize height", parseInt).option("--format <format>", "Output format (webp, jpeg, png, avif)", "webp").action(async (prompt, options) => {
|
|
884
|
+
const config = createConfig(options);
|
|
885
|
+
const generator = new ImageGenerator(config);
|
|
886
|
+
const spinner = ora("Generating image...").start();
|
|
887
|
+
try {
|
|
888
|
+
const result = await generator.generate({
|
|
889
|
+
prompt,
|
|
890
|
+
filename: options.filename,
|
|
891
|
+
category: options.category
|
|
892
|
+
});
|
|
893
|
+
if (result.success) {
|
|
894
|
+
spinner.succeed(chalk.green("Image generated!"));
|
|
895
|
+
console.log(chalk.gray(` Path: ${result.imagePath}`));
|
|
896
|
+
console.log(chalk.gray(` URL: ${result.imageUrl}`));
|
|
897
|
+
} else {
|
|
898
|
+
spinner.fail(chalk.red(result.error));
|
|
899
|
+
}
|
|
900
|
+
} catch (error) {
|
|
901
|
+
spinner.fail(chalk.red(error instanceof Error ? error.message : "Unknown error"));
|
|
902
|
+
process.exit(1);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
program.command("scan").alias("s").description("Scan and list all images").option("-r, --root <path>", "Project root directory").option("-d, --dirs <dirs>", "Directories to scan (comma-separated)", "static/images,images").option("--no-dimensions", "Skip reading image dimensions").action(async (options) => {
|
|
906
|
+
const config = createConfig(options);
|
|
907
|
+
const scanner = new ImageScanner(config);
|
|
908
|
+
const spinner = ora("Scanning images...").start();
|
|
909
|
+
try {
|
|
910
|
+
const result = await scanner.scan({
|
|
911
|
+
directories: options.dirs?.split(","),
|
|
912
|
+
includeDimensions: options.dimensions !== false
|
|
913
|
+
});
|
|
914
|
+
spinner.succeed(chalk.green(`Found ${result.total} images`));
|
|
915
|
+
console.log(chalk.gray(`
|
|
916
|
+
Total size: ${formatBytes(result.totalSize)}`));
|
|
917
|
+
console.log(chalk.gray(` Scan time: ${formatDuration(result.scanDuration)}`));
|
|
918
|
+
console.log(chalk.cyan("\n Categories:"));
|
|
919
|
+
for (const [category, images] of Object.entries(result.byCategory)) {
|
|
920
|
+
console.log(chalk.gray(` ${category}: ${images.length} images`));
|
|
921
|
+
}
|
|
922
|
+
console.log(chalk.cyan("\n Extensions:"));
|
|
923
|
+
for (const [ext, count] of Object.entries(result.byExtension)) {
|
|
924
|
+
console.log(chalk.gray(` .${ext}: ${count} files`));
|
|
925
|
+
}
|
|
926
|
+
} catch (error) {
|
|
927
|
+
spinner.fail(chalk.red(error instanceof Error ? error.message : "Unknown error"));
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
program.command("sync").description("Generate/update images.ts config file").option("-r, --root <path>", "Project root directory").option("-o, --output <path>", "Output path for images.ts", "src/core/images.ts").action(async (options) => {
|
|
932
|
+
const config = createConfig({ ...options, config: options.output });
|
|
933
|
+
const generator = new ConfigGenerator(config);
|
|
934
|
+
const spinner = ora("Generating images.ts...").start();
|
|
935
|
+
try {
|
|
936
|
+
const outputPath = await generator.generate();
|
|
937
|
+
spinner.succeed(chalk.green("Config synced!"));
|
|
938
|
+
console.log(chalk.gray(` Output: ${outputPath}`));
|
|
939
|
+
} catch (error) {
|
|
940
|
+
spinner.fail(chalk.red(error instanceof Error ? error.message : "Unknown error"));
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
program.command("batch").alias("b").description("Batch generate images from file").argument("<file>", "JSON file with prompts array").option("-r, --root <path>", "Project root directory").option("-c, --concurrency <num>", "Concurrent generations", "2").option("--category <name>", "Category for all images", "batch").action(async (file, options) => {
|
|
945
|
+
const config = createConfig(options);
|
|
946
|
+
const generator = new ImageGenerator(config);
|
|
947
|
+
const fs4 = await import('fs-extra');
|
|
948
|
+
const filePath = path2.resolve(process.cwd(), file);
|
|
949
|
+
if (!await fs4.pathExists(filePath)) {
|
|
950
|
+
console.error(chalk.red(`File not found: ${filePath}`));
|
|
951
|
+
process.exit(1);
|
|
952
|
+
}
|
|
953
|
+
const data = await fs4.readJson(filePath);
|
|
954
|
+
const prompts = Array.isArray(data) ? data : data.prompts;
|
|
955
|
+
if (!Array.isArray(prompts)) {
|
|
956
|
+
console.error(chalk.red("Invalid file format. Expected array or { prompts: [] }"));
|
|
957
|
+
process.exit(1);
|
|
958
|
+
}
|
|
959
|
+
console.log(chalk.cyan(`
|
|
960
|
+
Generating ${prompts.length} images...
|
|
961
|
+
`));
|
|
962
|
+
const result = await generator.generateBatch({
|
|
963
|
+
items: prompts.map((item, i) => ({
|
|
964
|
+
prompt: typeof item === "string" ? item : item.prompt,
|
|
965
|
+
filename: typeof item === "string" ? void 0 : item.filename,
|
|
966
|
+
category: options.category
|
|
967
|
+
})),
|
|
968
|
+
concurrency: parseInt(options.concurrency),
|
|
969
|
+
onProgress: (current, total, res) => {
|
|
970
|
+
const status = res.success ? chalk.green("\u2713") : chalk.red("\u2717");
|
|
971
|
+
console.log(` ${status} ${current}/${total} - ${res.imagePath || res.error}`);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
console.log(chalk.cyan("\n Batch complete!"));
|
|
975
|
+
console.log(chalk.gray(` Success: ${result.success}`));
|
|
976
|
+
console.log(chalk.gray(` Failed: ${result.failed}`));
|
|
977
|
+
console.log(chalk.gray(` Time: ${formatDuration(result.duration)}`));
|
|
978
|
+
});
|
|
979
|
+
program.parse();
|
|
980
|
+
//# sourceMappingURL=bin.js.map
|
|
981
|
+
//# sourceMappingURL=bin.js.map
|