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