myaidev-method 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/.claude-plugin/plugin.json +0 -1
  2. package/.env.example +5 -4
  3. package/CHANGELOG.md +2 -2
  4. package/CONTENT_CREATION_GUIDE.md +489 -3211
  5. package/DEVELOPER_USE_CASES.md +1 -1
  6. package/MODULAR_INSTALLATION.md +2 -2
  7. package/README.md +39 -33
  8. package/TECHNICAL_ARCHITECTURE.md +1 -1
  9. package/USER_GUIDE.md +242 -190
  10. package/agents/content-editor-agent.md +90 -0
  11. package/agents/content-planner-agent.md +97 -0
  12. package/agents/content-research-agent.md +62 -0
  13. package/agents/content-seo-agent.md +101 -0
  14. package/agents/content-writer-agent.md +69 -0
  15. package/agents/infographic-analyzer-agent.md +63 -0
  16. package/agents/infographic-designer-agent.md +72 -0
  17. package/bin/cli.js +776 -422
  18. package/{content-rules.example.md → content-rules-example.md} +2 -2
  19. package/dist/mcp/health-check.js +82 -68
  20. package/dist/mcp/mcp-config.json +8 -0
  21. package/dist/mcp/openstack-server.js +1746 -1262
  22. package/dist/server/.tsbuildinfo +1 -1
  23. package/extension.json +21 -4
  24. package/package.json +181 -184
  25. package/skills/company-config/SKILL.md +133 -0
  26. package/skills/configure/SKILL.md +1 -1
  27. package/skills/myai-configurator/SKILL.md +77 -0
  28. package/skills/myai-configurator/content-creation-configurator/SKILL.md +516 -0
  29. package/skills/myai-configurator/content-maintenance-configurator/SKILL.md +397 -0
  30. package/skills/myai-content-enrichment/SKILL.md +114 -0
  31. package/skills/myai-content-ideation/SKILL.md +288 -0
  32. package/skills/myai-content-ideation/evals/evals.json +182 -0
  33. package/skills/myai-content-production-coordinator/SKILL.md +946 -0
  34. package/skills/{content-rules-setup → myai-content-rules-setup}/SKILL.md +1 -1
  35. package/skills/{content-verifier → myai-content-verifier}/SKILL.md +1 -1
  36. package/skills/myai-content-writer/SKILL.md +333 -0
  37. package/skills/{infographic → myai-infographic}/SKILL.md +1 -1
  38. package/skills/myai-proprietary-content-verifier/SKILL.md +175 -0
  39. package/skills/myai-proprietary-content-verifier/evals/evals.json +36 -0
  40. package/skills/myai-skill-builder/SKILL.md +699 -0
  41. package/skills/myai-skill-builder/agents/analyzer-agent.md +137 -0
  42. package/skills/myai-skill-builder/agents/comparator-agent.md +77 -0
  43. package/skills/myai-skill-builder/agents/grader-agent.md +103 -0
  44. package/skills/myai-skill-builder/assets/eval_review.html +131 -0
  45. package/skills/myai-skill-builder/references/schemas.md +211 -0
  46. package/skills/myai-skill-builder/scripts/aggregate_benchmark.py +190 -0
  47. package/skills/myai-skill-builder/scripts/generate_review.py +381 -0
  48. package/skills/myai-skill-builder/scripts/package_skill.py +91 -0
  49. package/skills/myai-skill-builder/scripts/run_eval.py +105 -0
  50. package/skills/myai-skill-builder/scripts/run_loop.py +211 -0
  51. package/skills/myai-skill-builder/scripts/utils.py +123 -0
  52. package/skills/myai-visual-generator/SKILL.md +125 -0
  53. package/skills/myai-visual-generator/evals/evals.json +155 -0
  54. package/skills/myai-visual-generator/references/infographic-pipeline.md +73 -0
  55. package/skills/myai-visual-generator/references/research-visuals.md +57 -0
  56. package/skills/myai-visual-generator/references/services.md +89 -0
  57. package/skills/myai-visual-generator/scripts/visual-generation-utils.js +1272 -0
  58. package/skills/myaidev-figma/SKILL.md +212 -0
  59. package/skills/myaidev-figma/capture.js +133 -0
  60. package/skills/myaidev-figma/crawl.js +130 -0
  61. package/skills/myaidev-figma-configure/SKILL.md +130 -0
  62. package/skills/openstack-manager/SKILL.md +1 -1
  63. package/skills/payloadcms-publisher/SKILL.md +141 -77
  64. package/skills/payloadcms-publisher/references/field-mapping.md +142 -0
  65. package/skills/payloadcms-publisher/references/lexical-format.md +97 -0
  66. package/skills/security-auditor/SKILL.md +1 -1
  67. package/src/cli/commands/addon.js +105 -7
  68. package/src/config/workflows.js +172 -228
  69. package/src/lib/ascii-banner.js +197 -182
  70. package/src/lib/{content-coordinator.js → content-production-coordinator.js} +649 -459
  71. package/src/lib/installation-detector.js +93 -59
  72. package/src/lib/payloadcms-utils.js +285 -510
  73. package/src/lib/workflow-installer.js +55 -0
  74. package/src/mcp/health-check.js +82 -68
  75. package/src/mcp/openstack-server.js +1746 -1262
  76. package/src/scripts/configure-visual-apis.js +224 -173
  77. package/src/scripts/configure-wordpress-mcp.js +96 -66
  78. package/src/scripts/init/install.js +109 -85
  79. package/src/scripts/init-project.js +138 -67
  80. package/src/scripts/utils/write-content.js +67 -52
  81. package/src/scripts/wordpress/publish-to-wordpress.js +128 -128
  82. package/src/templates/claude/CLAUDE.md +19 -12
  83. package/hooks/hooks.json +0 -26
  84. package/skills/content-coordinator/SKILL.md +0 -130
  85. package/skills/content-enrichment/SKILL.md +0 -80
  86. package/skills/content-writer/SKILL.md +0 -285
  87. package/skills/skill-builder/SKILL.md +0 -417
  88. package/skills/visual-generator/SKILL.md +0 -140
  89. /package/skills/{content-writer → myai-content-writer}/agents/editor-agent.md +0 -0
  90. /package/skills/{content-writer → myai-content-writer}/agents/planner-agent.md +0 -0
  91. /package/skills/{content-writer → myai-content-writer}/agents/research-agent.md +0 -0
  92. /package/skills/{content-writer → myai-content-writer}/agents/seo-agent.md +0 -0
  93. /package/skills/{content-writer → myai-content-writer}/agents/visual-planner-agent.md +0 -0
  94. /package/skills/{content-writer → myai-content-writer}/agents/writer-agent.md +0 -0
@@ -0,0 +1,1272 @@
1
+ /**
2
+ * Visual Content Generation Utilities
3
+ *
4
+ * Provides image and video generation capabilities using:
5
+ *
6
+ * RECOMMENDED SOTA MODELS:
7
+ * - Google Gemini 3.0 Pro Image ("Nano Banana") - fast, cost-effective
8
+ * - OpenAI GPT Image 1.5 - state-of-the-art quality, best text rendering
9
+ *
10
+ * ADDITIONAL MODELS:
11
+ * - Google Imagen 3 - premium quality via Gemini API
12
+ * - Black Forest Labs FLUX 2 (pro, flex, dev) - excellent quality
13
+ * - Google Veo 3 - latest video generation
14
+ *
15
+ * Authentication: Uses simple API keys (GEMINI_API_KEY, OPENAI_API_KEY, FAL_KEY)
16
+ *
17
+ * Platform support: Claude Code, Gemini CLI, Codex CLI
18
+ *
19
+ * @module visual-generation-utils
20
+ */
21
+
22
+ import fs from "fs-extra";
23
+ import path from "path";
24
+ import dotenv from "dotenv";
25
+
26
+ dotenv.config();
27
+
28
+ // API Configuration
29
+ const GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta";
30
+ const OPENAI_API_BASE = "https://api.openai.com/v1";
31
+
32
+ // Gemini Models for image generation
33
+ const GEMINI_IMAGE_MODEL = "gemini-3-pro-image-preview"; // Gemini 3.0 "Nano Banana" preview
34
+ const GEMINI_IMAGEN_MODEL = "imagen-3.0-generate-002"; // Imagen via Gemini API
35
+
36
+ // OpenAI GPT Image Models (SOTA)
37
+ const OPENAI_IMAGE_MODELS = {
38
+ "gpt-image-1.5": "gpt-image-1.5", // State-of-the-art (recommended)
39
+ "gpt-image-1": "gpt-image-1", // Main model
40
+ "gpt-image-1-mini": "gpt-image-1-mini", // Cost-effective option
41
+ };
42
+
43
+ // FLUX 2 Models (via Fal.ai or BFL API)
44
+ export const FLUX2_MODELS = {
45
+ "flux2-pro": "fal-ai/flux-2-pro", // State-of-the-art quality, fastest, lowest cost
46
+ "flux2-flex": "fal-ai/flux-2-flex", // Developer-controlled parameters
47
+ "flux2-dev": "fal-ai/flux-2-dev", // 32B open-weight model
48
+ // Legacy FLUX 1.x models (still available)
49
+ "flux-pro": "fal-ai/flux-pro/v1.1-ultra",
50
+ "flux-dev": "fal-ai/flux/dev",
51
+
52
+ // nano banana models
53
+ "nano-banana-pro": "fal-ai/nano-banana-pro",
54
+ };
55
+
56
+ // Pricing (USD per image/video)
57
+ const PRICING = {
58
+ // SOTA Models (Recommended)
59
+ gemini: 0.02, // Gemini 3.0 Pro Image "Nano Banana" - fast, cheap
60
+ "gpt-image-1.5": 0.19, // OpenAI GPT Image 1.5 - SOTA quality (high quality)
61
+ "gpt-image-1.5-medium": 0.07, // GPT Image 1.5 medium quality
62
+ "gpt-image-1.5-low": 0.02, // GPT Image 1.5 low quality
63
+ "gpt-image-1": 0.19, // OpenAI GPT Image 1 (high quality)
64
+ "gpt-image-1-mini": 0.02, // OpenAI GPT Image 1 Mini - budget option
65
+ // Additional Models
66
+ imagen: 0.03, // Imagen 3 (Gemini API)
67
+ nano_banana_pro: 0.15, // Nano Banana Pro (Fal API)
68
+ // FLUX 2 pricing
69
+ flux2_pro: 0.05, // FLUX 2 Pro
70
+ flux2_flex: 0.04, // FLUX 2 Flex
71
+ flux2_dev: 0.025, // FLUX 2 Dev
72
+ // Legacy FLUX 1.x
73
+ flux_pro: 0.06, // FLUX Pro v1.1 Ultra
74
+ flux_dev: 0.025, // FLUX Dev (per megapixel)
75
+ // Video
76
+ veo3: 0.4, // Veo 3 (per second)
77
+ };
78
+
79
+ /**
80
+ * Validate that required API keys are configured
81
+ *
82
+ * @returns {Object} Validation results
83
+ */
84
+ export function validateAPIKeys() {
85
+ // Support both GEMINI_API_KEY (preferred) and GOOGLE_API_KEY (legacy)
86
+ const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
87
+ const openaiKey = process.env.OPENAI_API_KEY;
88
+ const falKey = process.env.FAL_KEY;
89
+ const bflKey = process.env.BFL_API_KEY; // Black Forest Labs direct API
90
+
91
+ const hasGemini = !!(geminiKey && geminiKey.length > 20);
92
+ const hasOpenAI = !!(openaiKey && openaiKey.length > 20);
93
+ const hasFal = !!(falKey && falKey.length > 20);
94
+ const hasBFL = !!(bflKey && bflKey.length > 20);
95
+
96
+ const availableServices = [];
97
+ if (hasGemini) {
98
+ availableServices.push("gemini", "imagen");
99
+ }
100
+ if (hasOpenAI) {
101
+ availableServices.push("gpt-image-1.5", "gpt-image-1", "gpt-image-1-mini");
102
+ }
103
+ if (hasFal || hasBFL) {
104
+ availableServices.push(
105
+ "nano-banana-pro",
106
+ "flux2-pro",
107
+ "flux2-flex",
108
+ "flux2-dev",
109
+ "flux-pro",
110
+ "flux-dev",
111
+ "veo3",
112
+ );
113
+ }
114
+
115
+ return {
116
+ hasGemini,
117
+ hasGoogle: hasGemini, // Legacy compatibility
118
+ hasOpenAI,
119
+ hasFal,
120
+ hasBFL,
121
+ hasAny: hasGemini || hasOpenAI || hasFal || hasBFL,
122
+ availableServices,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Estimate cost for image/video generation
128
+ *
129
+ * @param {string} service - Service name
130
+ * @param {Object} options - Generation options
131
+ * @returns {number} Estimated cost in USD
132
+ */
133
+ export function estimateCost(service, options = {}) {
134
+ const { quality = "high" } = options;
135
+
136
+ switch (service) {
137
+ case "gemini":
138
+ return PRICING.gemini;
139
+
140
+ case "imagen":
141
+ return PRICING.imagen;
142
+
143
+ // OpenAI GPT Image models - cost varies by quality
144
+ case "gpt-image-1.5":
145
+ if (quality === "low") return PRICING["gpt-image-1.5-low"];
146
+ if (quality === "medium") return PRICING["gpt-image-1.5-medium"];
147
+ return PRICING["gpt-image-1.5"]; // high quality default
148
+
149
+ case "gpt-image-1":
150
+ return PRICING["gpt-image-1"];
151
+
152
+ case "gpt-image-1-mini":
153
+ return PRICING["gpt-image-1-mini"];
154
+
155
+ case "flux2-pro":
156
+ return PRICING.flux2_pro;
157
+
158
+ case "flux2-flex":
159
+ return PRICING.flux2_flex;
160
+
161
+ case "flux2-dev":
162
+ return PRICING.flux2_dev;
163
+
164
+ case "flux":
165
+ case "flux-pro":
166
+ return PRICING.flux_pro;
167
+
168
+ case "flux-dev":
169
+ return PRICING.flux_dev;
170
+
171
+ case "nano-banana-pro":
172
+ return PRICING.nano_banana_pro;
173
+
174
+ case "veo3":
175
+ case "veo3-fast":
176
+ return PRICING.veo3; // per second, will multiply by duration
177
+
178
+ default:
179
+ return 0;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Select best available service based on preferences
185
+ *
186
+ * @param {string} preferred - Preferred service name
187
+ * @returns {string} Selected service name
188
+ * @throws {Error} If no API keys are configured
189
+ */
190
+ export function selectBestService(preferred = "gemini") {
191
+ const { availableServices, hasAny } = validateAPIKeys();
192
+
193
+ if (!hasAny) {
194
+ throw new Error(
195
+ "No API keys configured. Set GEMINI_API_KEY, OPENAI_API_KEY, or FAL_KEY in your environment.",
196
+ );
197
+ }
198
+
199
+ // Return preferred service if available
200
+ if (availableServices.includes(preferred)) {
201
+ return preferred;
202
+ }
203
+
204
+ // Fallback to first available service
205
+ return availableServices[0];
206
+ }
207
+
208
+ /**
209
+ * Generate image using Google Gemini API
210
+ * Uses gemini-3-pro-image-preview model ("Nano Banana") with simple API key auth
211
+ *
212
+ * @param {string} prompt - Image description
213
+ * @param {Object} options - Generation options
214
+ * @param {string} options.imageSize - Image size (1K, 2K)
215
+ * @param {number} options.maxRetries - Maximum retry attempts
216
+ * @returns {Promise<Object>} Generated image data
217
+ */
218
+ export async function generateImageGemini(prompt, options = {}) {
219
+ const { imageSize = "1K", maxRetries = 3 } = options;
220
+
221
+ const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
222
+ if (!apiKey) {
223
+ throw new Error(
224
+ "GEMINI_API_KEY not configured. Set GEMINI_API_KEY in your environment.",
225
+ );
226
+ }
227
+
228
+ const endpoint = `${GEMINI_API_BASE}/models/${GEMINI_IMAGE_MODEL}:generateContent`;
229
+
230
+ const requestBody = {
231
+ contents: [
232
+ {
233
+ role: "user",
234
+ parts: [
235
+ {
236
+ text: prompt,
237
+ },
238
+ ],
239
+ },
240
+ ],
241
+ generationConfig: {
242
+ responseModalities: ["IMAGE", "TEXT"],
243
+ imageConfig: {
244
+ image_size: imageSize,
245
+ },
246
+ },
247
+ };
248
+
249
+ let lastError;
250
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
251
+ try {
252
+ const response = await fetch(`${endpoint}?key=${apiKey}`, {
253
+ method: "POST",
254
+ headers: {
255
+ "Content-Type": "application/json",
256
+ },
257
+ body: JSON.stringify(requestBody),
258
+ });
259
+
260
+ if (!response.ok) {
261
+ const errorText = await response.text();
262
+ throw new Error(`Gemini API error: ${response.status} - ${errorText}`);
263
+ }
264
+
265
+ const data = await response.json();
266
+
267
+ // Handle streaming response format (array of candidates)
268
+ const candidates = Array.isArray(data) ? data : data.candidates || [data];
269
+
270
+ for (const candidate of candidates) {
271
+ const content = candidate.content || candidate;
272
+ const parts = content.parts || [];
273
+
274
+ for (const part of parts) {
275
+ // Check for inline image data
276
+ if (part.inlineData && part.inlineData.data) {
277
+ return {
278
+ data: part.inlineData.data,
279
+ mimeType: part.inlineData.mimeType || "image/png",
280
+ service: "gemini",
281
+ model: GEMINI_IMAGE_MODEL,
282
+ cost: PRICING.gemini,
283
+ };
284
+ }
285
+
286
+ // Check for file data reference
287
+ if (part.fileData && part.fileData.fileUri) {
288
+ const imageResponse = await fetch(part.fileData.fileUri);
289
+ const imageBuffer = await imageResponse.arrayBuffer();
290
+ const base64Data = Buffer.from(imageBuffer).toString("base64");
291
+
292
+ return {
293
+ data: base64Data,
294
+ mimeType: part.fileData.mimeType || "image/png",
295
+ service: "gemini",
296
+ model: GEMINI_IMAGE_MODEL,
297
+ cost: PRICING.gemini,
298
+ };
299
+ }
300
+ }
301
+ }
302
+
303
+ throw new Error("No image data in Gemini response");
304
+ } catch (error) {
305
+ lastError = error;
306
+
307
+ if (attempt < maxRetries) {
308
+ const backoff = Math.pow(2, attempt) * 1000;
309
+ console.log(
310
+ `⚠️ Gemini attempt ${attempt} failed: ${error.message}. Retrying in ${backoff / 1000}s...`,
311
+ );
312
+ await sleep(backoff);
313
+ }
314
+ }
315
+ }
316
+
317
+ throw lastError;
318
+ }
319
+
320
+ /**
321
+ * Generate image using Google Imagen 3 (via Gemini API)
322
+ * Premium quality image generation with simple API key authentication
323
+ *
324
+ * @param {string} prompt - Image description
325
+ * @param {Object} options - Generation options
326
+ * @param {string} options.aspectRatio - Aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4)
327
+ * @param {number} options.numberOfImages - Number of images to generate (1-4)
328
+ * @param {number} options.maxRetries - Maximum retry attempts
329
+ * @returns {Promise<Object>} Generated image data
330
+ */
331
+ export async function generateImageImagen(prompt, options = {}) {
332
+ const { aspectRatio = "1:1", numberOfImages = 1, maxRetries = 3 } = options;
333
+
334
+ const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
335
+ if (!apiKey) {
336
+ throw new Error(
337
+ "GEMINI_API_KEY not configured. Set GEMINI_API_KEY in your environment.",
338
+ );
339
+ }
340
+
341
+ const endpoint = `${GEMINI_API_BASE}/models/${GEMINI_IMAGEN_MODEL}:generateImages`;
342
+
343
+ const requestBody = {
344
+ prompt: prompt,
345
+ config: {
346
+ numberOfImages: Math.min(numberOfImages, 4),
347
+ aspectRatio: aspectRatio,
348
+ safetyFilterLevel: "BLOCK_MEDIUM_AND_ABOVE",
349
+ },
350
+ };
351
+
352
+ let lastError;
353
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
354
+ try {
355
+ const response = await fetch(`${endpoint}?key=${apiKey}`, {
356
+ method: "POST",
357
+ headers: {
358
+ "Content-Type": "application/json",
359
+ },
360
+ body: JSON.stringify(requestBody),
361
+ });
362
+
363
+ if (!response.ok) {
364
+ const errorText = await response.text();
365
+ throw new Error(`Imagen API error: ${response.status} - ${errorText}`);
366
+ }
367
+
368
+ const data = await response.json();
369
+
370
+ // Imagen returns images array with base64 encoded data
371
+ if (data.generatedImages && data.generatedImages[0]) {
372
+ const image = data.generatedImages[0];
373
+
374
+ if (image.image && image.image.imageBytes) {
375
+ return {
376
+ data: image.image.imageBytes,
377
+ mimeType: "image/png",
378
+ service: "imagen",
379
+ model: GEMINI_IMAGEN_MODEL,
380
+ cost: PRICING.imagen,
381
+ };
382
+ }
383
+ }
384
+
385
+ // Alternative response format
386
+ if (data.images && data.images[0]) {
387
+ const image = data.images[0];
388
+
389
+ if (image.bytesBase64Encoded || image.imageBytes) {
390
+ return {
391
+ data: image.bytesBase64Encoded || image.imageBytes,
392
+ mimeType: "image/png",
393
+ service: "imagen",
394
+ model: GEMINI_IMAGEN_MODEL,
395
+ cost: PRICING.imagen,
396
+ };
397
+ }
398
+ }
399
+
400
+ throw new Error("No image data in Imagen response");
401
+ } catch (error) {
402
+ lastError = error;
403
+
404
+ if (attempt < maxRetries) {
405
+ const backoff = Math.pow(2, attempt) * 1000;
406
+ console.log(
407
+ `⚠️ Imagen attempt ${attempt} failed: ${error.message}. Retrying in ${backoff / 1000}s...`,
408
+ );
409
+ await sleep(backoff);
410
+ }
411
+ }
412
+ }
413
+
414
+ throw lastError;
415
+ }
416
+
417
+ /**
418
+ * Generate image using OpenAI GPT Image API
419
+ * State-of-the-art image generation with best text rendering
420
+ *
421
+ * Features:
422
+ * - Best-in-class text rendering in images
423
+ * - Multiple quality tiers (low, medium, high)
424
+ * - Transparency support (PNG with transparent background)
425
+ * - Multiple output formats (PNG, JPEG, WebP)
426
+ *
427
+ * @param {string} prompt - Image description
428
+ * @param {Object} options - Generation options
429
+ * @param {string} options.model - Model (gpt-image-1.5, gpt-image-1, gpt-image-1-mini)
430
+ * @param {string} options.size - Image size (1024x1024, 1536x1024, 1024x1536, auto)
431
+ * @param {string} options.quality - Quality level (low, medium, high, auto)
432
+ * @param {string} options.outputFormat - Output format (png, jpeg, webp)
433
+ * @param {string} options.background - Background type (transparent, opaque, auto)
434
+ * @param {number} options.maxRetries - Maximum retry attempts
435
+ * @returns {Promise<Object>} Generated image data
436
+ */
437
+ export async function generateImageOpenAI(prompt, options = {}) {
438
+ const {
439
+ model = "gpt-image-1.5",
440
+ size = "1024x1024",
441
+ quality = "high",
442
+ outputFormat = "png",
443
+ background = "auto",
444
+ maxRetries = 3,
445
+ } = options;
446
+
447
+ const apiKey = process.env.OPENAI_API_KEY;
448
+ if (!apiKey) {
449
+ throw new Error(
450
+ "OPENAI_API_KEY not configured. Get your key from https://platform.openai.com/api-keys",
451
+ );
452
+ }
453
+
454
+ const endpoint = `${OPENAI_API_BASE}/images/generations`;
455
+
456
+ const requestBody = {
457
+ model: OPENAI_IMAGE_MODELS[model] || model,
458
+ prompt: prompt,
459
+ n: 1,
460
+ size: size,
461
+ quality: quality,
462
+ output_format: outputFormat,
463
+ };
464
+
465
+ // Add background for PNG format (transparency support)
466
+ if (outputFormat === "png" && background !== "auto") {
467
+ requestBody.background = background;
468
+ }
469
+
470
+ let lastError;
471
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
472
+ try {
473
+ const response = await fetch(endpoint, {
474
+ method: "POST",
475
+ headers: {
476
+ "Content-Type": "application/json",
477
+ Authorization: `Bearer ${apiKey}`,
478
+ },
479
+ body: JSON.stringify(requestBody),
480
+ });
481
+
482
+ if (!response.ok) {
483
+ const errorText = await response.text();
484
+ let errorMessage = `OpenAI API error: ${response.status}`;
485
+ try {
486
+ const errorData = JSON.parse(errorText);
487
+ errorMessage = errorData.error?.message || errorMessage;
488
+ } catch {
489
+ errorMessage = `${errorMessage} - ${errorText}`;
490
+ }
491
+ throw new Error(errorMessage);
492
+ }
493
+
494
+ const data = await response.json();
495
+
496
+ // OpenAI returns base64-encoded image data
497
+ if (data.data && data.data[0]) {
498
+ const imageData = data.data[0];
499
+
500
+ // Handle base64 response (primary)
501
+ if (imageData.b64_json) {
502
+ return {
503
+ data: imageData.b64_json,
504
+ mimeType:
505
+ outputFormat === "jpeg"
506
+ ? "image/jpeg"
507
+ : outputFormat === "webp"
508
+ ? "image/webp"
509
+ : "image/png",
510
+ service: "openai",
511
+ model: model,
512
+ cost: estimateCost(model, { quality }),
513
+ revisedPrompt: imageData.revised_prompt,
514
+ };
515
+ }
516
+
517
+ // Handle URL response (fallback)
518
+ if (imageData.url) {
519
+ const imageResponse = await fetch(imageData.url);
520
+ const imageBuffer = await imageResponse.arrayBuffer();
521
+ const base64Data = Buffer.from(imageBuffer).toString("base64");
522
+
523
+ return {
524
+ data: base64Data,
525
+ mimeType:
526
+ outputFormat === "jpeg"
527
+ ? "image/jpeg"
528
+ : outputFormat === "webp"
529
+ ? "image/webp"
530
+ : "image/png",
531
+ service: "openai",
532
+ model: model,
533
+ cost: estimateCost(model, { quality }),
534
+ revisedPrompt: imageData.revised_prompt,
535
+ };
536
+ }
537
+ }
538
+
539
+ throw new Error("No image data in OpenAI response");
540
+ } catch (error) {
541
+ lastError = error;
542
+
543
+ if (attempt < maxRetries) {
544
+ const backoff = Math.pow(2, attempt) * 1000;
545
+ console.log(
546
+ `⚠️ OpenAI attempt ${attempt} failed: ${error.message}. Retrying in ${backoff / 1000}s...`,
547
+ );
548
+ await sleep(backoff);
549
+ }
550
+ }
551
+ }
552
+
553
+ throw lastError;
554
+ }
555
+
556
+ /**
557
+ * Generate image using FLUX 2 (via Fal.ai)
558
+ * State-of-the-art image generation from Black Forest Labs
559
+ *
560
+ * Features:
561
+ * - Multi-reference support (up to 10 images)
562
+ * - Enhanced photorealism
563
+ * - Complex typography and UI mockups
564
+ * - Image editing up to 4 megapixels
565
+ *
566
+ * @param {string} prompt - Image description
567
+ * @param {Object} options - Generation options
568
+ * @param {string} options.model - FLUX 2 model (flux2-pro, flux2-flex, flux2-dev)
569
+ * @param {string} options.size - Image size (square, landscape, portrait)
570
+ * @param {number} options.steps - Number of inference steps (flux2-flex only)
571
+ * @param {number} options.guidance - Guidance scale (flux2-flex only)
572
+ * @param {Array<string>} options.referenceImages - Reference image URLs (up to 10)
573
+ * @param {number} options.maxRetries - Maximum retry attempts
574
+ * @returns {Promise<Object>} Generated image data
575
+ */
576
+ export async function generateImageFlux2(prompt, options = {}) {
577
+ const {
578
+ model = "flux2-pro",
579
+ size = "square",
580
+ steps = 28,
581
+ guidance = 3.5,
582
+ referenceImages = [],
583
+ maxRetries = 3,
584
+ } = options;
585
+
586
+ const apiKey = process.env.FAL_KEY || process.env.BFL_API_KEY;
587
+ if (!apiKey) {
588
+ throw new Error(
589
+ "FAL_KEY not configured. Get your key from https://fal.ai/dashboard/keys",
590
+ );
591
+ }
592
+
593
+ // Import fal.ai client
594
+ const { fal } = await import("@fal-ai/client");
595
+ fal.config({ credentials: apiKey });
596
+
597
+ // Get endpoint for model
598
+ const endpoint = FLUX2_MODELS[model] || FLUX2_MODELS["flux2-pro"];
599
+
600
+ // Build input based on model capabilities
601
+ const input = {
602
+ prompt: prompt,
603
+ image_size: size === "1024x1024" ? "square" : size,
604
+ num_images: 1,
605
+ };
606
+
607
+ // FLUX 2 Flex supports custom parameters
608
+ if (model === "flux2-flex") {
609
+ input.num_inference_steps = steps;
610
+ input.guidance_scale = guidance;
611
+ }
612
+
613
+ // Add reference images if provided (FLUX 2 multi-reference feature)
614
+ if (referenceImages.length > 0) {
615
+ input.reference_images = referenceImages.slice(0, 10); // Max 10
616
+ }
617
+
618
+ let lastError;
619
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
620
+ try {
621
+ const result = await fal.subscribe(endpoint, {
622
+ input,
623
+ logs: false,
624
+ });
625
+
626
+ // Extract image from result
627
+ let imageUrl;
628
+ let contentType = "image/png";
629
+
630
+ if (result.data?.images?.[0]) {
631
+ imageUrl = result.data.images[0].url;
632
+ contentType = result.data.images[0].content_type || "image/png";
633
+ } else if (result.images?.[0]) {
634
+ imageUrl = result.images[0].url;
635
+ contentType = result.images[0].content_type || "image/png";
636
+ } else if (result.image?.url) {
637
+ imageUrl = result.image.url;
638
+ }
639
+
640
+ if (imageUrl) {
641
+ // Fetch and convert to base64
642
+ const imageResponse = await fetch(imageUrl);
643
+ const imageBuffer = await imageResponse.arrayBuffer();
644
+ const base64Data = Buffer.from(imageBuffer).toString("base64");
645
+
646
+ return {
647
+ data: base64Data,
648
+ mimeType: contentType,
649
+ service: "flux2",
650
+ model: model,
651
+ cost: PRICING[model.replaceAll("-", "_")] || PRICING.flux2_pro,
652
+ };
653
+ }
654
+
655
+ throw new Error("No image data in FLUX 2 response");
656
+ } catch (error) {
657
+ lastError = error;
658
+
659
+ if (attempt < maxRetries) {
660
+ const backoff = Math.pow(2, attempt) * 1000;
661
+ console.log(
662
+ `⚠️ FLUX 2 attempt ${attempt} failed: ${error.message}. Retrying in ${backoff / 1000}s...`,
663
+ );
664
+ await sleep(backoff);
665
+ }
666
+ }
667
+ }
668
+
669
+ throw lastError;
670
+ }
671
+
672
+ /**
673
+ * Generate image using legacy FLUX 1.x (via Fal.ai)
674
+ * Still available for backwards compatibility
675
+ *
676
+ * @param {string} prompt - Image description
677
+ * @param {Object} options - Generation options
678
+ * @param {string} options.model - FLUX model (nano-banana-pro, flux-pro, flux-dev)
679
+ * @param {string} options.size - Image size
680
+ * @param {number} options.maxRetries - Maximum retry attempts
681
+ * @returns {Promise<Object>} Generated image data
682
+ */
683
+ export async function generateImageFal(prompt, options = {}) {
684
+ const {
685
+ model = "nano-banana-pro",
686
+ size = "1024x1024",
687
+ maxRetries = 3,
688
+ } = options;
689
+
690
+ const apiKey = process.env.FAL_KEY;
691
+ if (!apiKey) {
692
+ throw new Error(
693
+ "FAL_KEY not configured. Get your key from https://fal.ai/dashboard/keys",
694
+ );
695
+ }
696
+
697
+ const { fal } = await import("@fal-ai/client");
698
+ fal.config({ credentials: apiKey });
699
+
700
+ const endpoint = FLUX2_MODELS[model] || FLUX2_MODELS["flux-pro"];
701
+
702
+ let lastError;
703
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
704
+ try {
705
+ const result = await fal.subscribe(endpoint, {
706
+ input: {
707
+ prompt: prompt,
708
+ image_size: size === "1024x1024" ? "square" : "landscape",
709
+ num_images: 1,
710
+ },
711
+ logs: false,
712
+ });
713
+
714
+ let imageUrl;
715
+ let contentType = "image/png";
716
+
717
+ if (result.data?.images?.[0]) {
718
+ imageUrl = result.data.images[0].url;
719
+ contentType = result.data.images[0].content_type || "image/png";
720
+ } else if (result.images?.[0]) {
721
+ imageUrl = result.images[0].url;
722
+ contentType = result.images[0].content_type || "image/png";
723
+ } else if (result.image?.url) {
724
+ imageUrl = result.image.url;
725
+ }
726
+
727
+ if (imageUrl) {
728
+ const imageResponse = await fetch(imageUrl);
729
+ const imageBuffer = await imageResponse.arrayBuffer();
730
+ const base64Data = Buffer.from(imageBuffer).toString("base64");
731
+
732
+ return {
733
+ data: base64Data,
734
+ mimeType: contentType,
735
+ service: "fal",
736
+ model: model,
737
+ cost: PRICING[model.replaceAll("-", "_")] || PRICING.flux_pro,
738
+ };
739
+ }
740
+
741
+ throw new Error("No image data in Fal.ai response");
742
+ } catch (error) {
743
+ lastError = error;
744
+
745
+ if (attempt < maxRetries) {
746
+ const backoff = Math.pow(2, attempt) * 1000;
747
+ console.log(
748
+ `⚠️ Fal.ai attempt ${attempt} failed: ${error.message}. Retrying in ${backoff / 1000}s...`,
749
+ );
750
+ await sleep(backoff);
751
+ }
752
+ }
753
+ }
754
+
755
+ throw lastError;
756
+ }
757
+
758
+ /**
759
+ * Generate video using Veo 3 (via Fal.ai)
760
+ * Latest video generation with outstanding quality
761
+ *
762
+ * @param {string} prompt - Video description
763
+ * @param {Object} options - Generation options
764
+ * @param {string} options.model - Model (veo3, veo3-fast)
765
+ * @param {number} options.duration - Video duration in seconds
766
+ * @param {string} options.aspectRatio - Aspect ratio
767
+ * @param {number} options.maxRetries - Maximum retry attempts
768
+ * @returns {Promise<Object>} Generated video data
769
+ */
770
+ export async function generateVideoVeo3(prompt, options = {}) {
771
+ const {
772
+ model = "veo3",
773
+ duration = 5,
774
+ aspectRatio = "16:9",
775
+ maxRetries = 3,
776
+ } = options;
777
+
778
+ const apiKey = process.env.FAL_KEY;
779
+ if (!apiKey) {
780
+ throw new Error(
781
+ "FAL_KEY not configured. Get your key from https://fal.ai/dashboard/keys",
782
+ );
783
+ }
784
+
785
+ const { fal } = await import("@fal-ai/client");
786
+ fal.config({ credentials: apiKey });
787
+
788
+ const endpoint = model === "veo3-fast" ? "fal-ai/veo3-fast" : "fal-ai/veo3";
789
+
790
+ let lastError;
791
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
792
+ try {
793
+ const result = await fal.subscribe(endpoint, {
794
+ input: {
795
+ prompt: prompt,
796
+ duration: Math.min(duration, 10),
797
+ aspect_ratio: aspectRatio,
798
+ },
799
+ logs: false,
800
+ });
801
+
802
+ if (result.video?.url) {
803
+ const videoResponse = await fetch(result.video.url);
804
+ const videoBuffer = await videoResponse.arrayBuffer();
805
+ const base64Data = Buffer.from(videoBuffer).toString("base64");
806
+
807
+ return {
808
+ data: base64Data,
809
+ url: result.video.url,
810
+ mimeType: "video/mp4",
811
+ service: "veo3",
812
+ model: model,
813
+ cost: PRICING.veo3 * duration,
814
+ duration: duration,
815
+ };
816
+ }
817
+
818
+ throw new Error("No video data in Veo 3 response");
819
+ } catch (error) {
820
+ lastError = error;
821
+
822
+ if (attempt < maxRetries) {
823
+ const backoff = Math.pow(2, attempt) * 1000;
824
+ console.log(
825
+ `⚠️ Veo 3 attempt ${attempt} failed: ${error.message}. Retrying in ${backoff / 1000}s...`,
826
+ );
827
+ await sleep(backoff);
828
+ }
829
+ }
830
+ }
831
+
832
+ throw lastError;
833
+ }
834
+
835
+ /**
836
+ * Download image from URL and return buffer
837
+ *
838
+ * @param {string} url - Image URL
839
+ * @returns {Promise<Buffer>} Image buffer
840
+ */
841
+ export async function downloadImage(url) {
842
+ const response = await fetch(url);
843
+
844
+ if (!response.ok) {
845
+ throw new Error(`Failed to download image: ${response.status}`);
846
+ }
847
+
848
+ const arrayBuffer = await response.arrayBuffer();
849
+ return Buffer.from(arrayBuffer);
850
+ }
851
+
852
+ /**
853
+ * Generate image using auto-selected service
854
+ * Automatically selects best available service based on preferences
855
+ *
856
+ * @param {string} prompt - Image description
857
+ * @param {Object} options - Generation options
858
+ * @param {string} options.preferredService - Preferred service
859
+ * @param {string} options.type - Image type for optimization (hero, illustration, diagram)
860
+ * @returns {Promise<Object>} Generated image data with buffer
861
+ */
862
+ export async function generateImage(prompt, options = {}) {
863
+ const { preferredService, type = "general", ...serviceOptions } = options;
864
+
865
+ // If a model is explicitly specified, use it as the service
866
+ // This allows --service=fal --model=nano-banana-pro to work correctly
867
+ let service;
868
+ if (serviceOptions.model && FLUX2_MODELS[serviceOptions.model]) {
869
+ service = serviceOptions.model;
870
+ } else {
871
+ // Select service normally
872
+ const defaultService = process.env.VISUAL_DEFAULT_SERVICE || "gemini";
873
+ service = selectBestService(preferredService || defaultService);
874
+ }
875
+
876
+ const modelInfo = serviceOptions.model
877
+ ? ` ${serviceOptions.model}`
878
+ : ` ${service}`;
879
+ console.log(`🎨 Generating ${type} image using${modelInfo}...`);
880
+
881
+ // Enhance prompt based on image type
882
+ const enhancedPrompt = enhancePrompt(prompt, type);
883
+
884
+ // Generate based on service
885
+ let result;
886
+ switch (service) {
887
+ case "gemini":
888
+ result = await generateImageGemini(enhancedPrompt, serviceOptions);
889
+ break;
890
+
891
+ case "imagen":
892
+ result = await generateImageImagen(enhancedPrompt, serviceOptions);
893
+ break;
894
+
895
+ // OpenAI GPT Image models (SOTA)
896
+ case "gpt-image-1.5":
897
+ case "gpt-image-1":
898
+ case "gpt-image-1-mini":
899
+ result = await generateImageOpenAI(enhancedPrompt, {
900
+ ...serviceOptions,
901
+ model: service,
902
+ });
903
+ break;
904
+
905
+ case "flux2-pro":
906
+ case "flux2-flex":
907
+ case "flux2-dev":
908
+ result = await generateImageFlux2(enhancedPrompt, {
909
+ ...serviceOptions,
910
+ model: service,
911
+ });
912
+ break;
913
+
914
+ case "flux":
915
+ case "flux-pro":
916
+ case "flux-dev":
917
+ case "nano-banana-pro":
918
+ result = await generateImageFal(enhancedPrompt, {
919
+ ...serviceOptions,
920
+ model: serviceOptions.model || service,
921
+ });
922
+ break;
923
+
924
+ default:
925
+ throw new Error(`Unknown service: ${service}`);
926
+ }
927
+
928
+ // Convert to buffer
929
+ let buffer;
930
+ if (result.data) {
931
+ buffer = Buffer.from(result.data, "base64");
932
+ } else if (result.url) {
933
+ buffer = await downloadImage(result.url);
934
+ } else {
935
+ throw new Error("No image data or URL in response");
936
+ }
937
+
938
+ return {
939
+ ...result,
940
+ buffer,
941
+ prompt: enhancedPrompt,
942
+ originalPrompt: prompt,
943
+ };
944
+ }
945
+
946
+ /**
947
+ * Generate video using auto-selected service
948
+ *
949
+ * @param {string} prompt - Video description
950
+ * @param {Object} options - Generation options
951
+ * @returns {Promise<Object>} Generated video data
952
+ */
953
+ export async function generateVideo(prompt, options = {}) {
954
+ const { preferredService = "veo3", ...serviceOptions } = options;
955
+
956
+ console.log(`🎬 Generating video using ${preferredService}...`);
957
+
958
+ return await generateVideoVeo3(prompt, serviceOptions);
959
+ }
960
+
961
+ /**
962
+ * Enhance prompt based on image type
963
+ *
964
+ * @param {string} prompt - Original prompt
965
+ * @param {string} type - Image type (hero, illustration, diagram, infographic-*, etc.)
966
+ * @returns {string} Enhanced prompt
967
+ */
968
+ function enhancePrompt(prompt, type) {
969
+ const enhancements = {
970
+ // Standard types
971
+ hero: 'Professional hero image, high quality, visually striking, suitable for article header:',
972
+ illustration: 'Clean illustration, professional style, clear and informative:',
973
+ diagram: 'Technical diagram, clear labels, professional design, easy to understand:',
974
+ screenshot: 'Professional screenshot, clean interface, high resolution:',
975
+
976
+ // Infographic types (optimized for GPT Image 1.5 text rendering)
977
+ 'infographic-data': 'Clean data visualization infographic with clear large typography, color-coded sections, modern flat design. Include prominent title area, 3-5 data callouts with large numbers, clean minimal layout, professional business style:',
978
+ 'infographic-process': 'Step-by-step process infographic with clearly numbered steps, simple icons for each step, connecting arrows between steps, clean modern design, horizontal or vertical flow layout, each step clearly labeled with action text:',
979
+ 'infographic-comparison': 'Side-by-side comparison infographic with two distinct columns, clear header labels, aligned comparison points, checkmarks for advantages, X marks for disadvantages, professional business style, easy to scan layout:',
980
+ 'infographic-timeline': 'Horizontal timeline infographic with dated milestone markers, small icons at each event point, connecting timeline line, clean modern design, clear date labels, brief event descriptions:',
981
+
982
+ // Technical diagram types
983
+ 'architecture-diagram': 'Technical system architecture diagram with labeled component boxes, directional connection arrows, cloud/server/database icons where appropriate, clear legend area, isometric or clean flat technical illustration style:',
984
+ 'flowchart': 'Professional flowchart with standard shapes - diamonds for decisions, rectangles for processes, ovals for start/end points, clear yes/no branching paths, labeled arrows, clean professional style:',
985
+ 'sequence-diagram': 'Technical sequence diagram showing component interactions with participant boxes at top, vertical lifelines, horizontal arrows with action labels, activation boxes, clean UML-style presentation:',
986
+
987
+ // Default
988
+ general: 'High quality image, professional style:'
989
+ };
990
+
991
+ const prefix = enhancements[type] || enhancements.general;
992
+ return `${prefix} ${prompt}`;
993
+ }
994
+
995
+ /**
996
+ * Get recommended service for image type
997
+ * Uses user's configured default service if available, otherwise falls back
998
+ * to first available service. Both Gemini and GPT Image 1.5 are excellent
999
+ * for text rendering - let the user choose based on their preference.
1000
+ *
1001
+ * @param {string} type - Image type (used for logging/future enhancements)
1002
+ * @param {Array} availableServices - Optional pre-computed available services
1003
+ * @returns {string} Recommended service name
1004
+ */
1005
+ export function getRecommendedServiceForType(type, availableServices = null) {
1006
+ // Get available services if not provided
1007
+ if (!availableServices) {
1008
+ const validation = validateAPIKeys();
1009
+ availableServices = validation.availableServices;
1010
+ }
1011
+
1012
+ // Get user's configured default service
1013
+ const userDefault = process.env.VISUAL_DEFAULT_SERVICE;
1014
+
1015
+ // If user has a configured default and it's available, use it
1016
+ if (userDefault && availableServices.includes(userDefault)) {
1017
+ return userDefault;
1018
+ }
1019
+
1020
+ // Otherwise return first available service
1021
+ if (availableServices.length > 0) {
1022
+ return availableServices[0];
1023
+ }
1024
+
1025
+ // Fallback (will likely fail without API keys, but maintains API compatibility)
1026
+ return 'gemini';
1027
+ }
1028
+
1029
+ /**
1030
+ * Build structured infographic prompt from data
1031
+ *
1032
+ * @param {Object} config - Infographic configuration
1033
+ * @param {string} config.type - Infographic type
1034
+ * @param {string} config.title - Infographic title
1035
+ * @param {Array} config.data - Data points or steps
1036
+ * @param {string} config.style - Style preference
1037
+ * @returns {string} Structured prompt
1038
+ */
1039
+ export function buildInfographicPrompt(config) {
1040
+ const { type, title, data = [], style = 'modern flat design' } = config;
1041
+
1042
+ let prompt = '';
1043
+
1044
+ switch (type) {
1045
+ case 'infographic-data':
1046
+ prompt = `Data visualization infographic titled "${title}". `;
1047
+ prompt += `Display these metrics prominently: `;
1048
+ prompt += data.map(d => `${d.label}: ${d.value}`).join(', ') + '. ';
1049
+ prompt += `Style: ${style}, clear typography, color-coded sections.`;
1050
+ break;
1051
+
1052
+ case 'infographic-process':
1053
+ prompt = `Process flow infographic titled "${title}". `;
1054
+ prompt += `Show these steps in sequence: `;
1055
+ prompt += data.map((step, i) => `Step ${i + 1}: ${step}`).join('; ') + '. ';
1056
+ prompt += `Style: numbered steps with icons, connecting arrows, ${style}.`;
1057
+ break;
1058
+
1059
+ case 'infographic-comparison':
1060
+ prompt = `Comparison infographic titled "${title}". `;
1061
+ prompt += `Compare these aspects: `;
1062
+ prompt += data.map(d => `${d.category} - Option A: ${d.optionA}, Option B: ${d.optionB}`).join('; ') + '. ';
1063
+ prompt += `Style: two-column layout, checkmarks for strengths, ${style}.`;
1064
+ break;
1065
+
1066
+ case 'infographic-timeline':
1067
+ prompt = `Timeline infographic titled "${title}". `;
1068
+ prompt += `Show these milestones: `;
1069
+ prompt += data.map(d => `${d.date}: ${d.event}`).join('; ') + '. ';
1070
+ prompt += `Style: horizontal timeline, dated markers, ${style}.`;
1071
+ break;
1072
+
1073
+ default:
1074
+ prompt = `Infographic: ${title}. ${data.join(', ')}. Style: ${style}.`;
1075
+ }
1076
+
1077
+ return prompt;
1078
+ }
1079
+
1080
+ /**
1081
+ * Build structured architecture diagram prompt
1082
+ *
1083
+ * @param {Object} config - Diagram configuration
1084
+ * @param {string} config.title - Diagram title
1085
+ * @param {Array} config.components - System components
1086
+ * @param {Array} config.connections - Component connections
1087
+ * @param {string} config.style - Style preference
1088
+ * @returns {string} Structured prompt
1089
+ */
1090
+ export function buildArchitectureDiagramPrompt(config) {
1091
+ const { title, components = [], connections = [], style = 'isometric technical' } = config;
1092
+
1093
+ let prompt = `Technical architecture diagram: "${title}". `;
1094
+ prompt += `Components: ${components.join(', ')}. `;
1095
+ prompt += `Connections: ${connections.join('; ')}. `;
1096
+ prompt += `Style: ${style}, labeled boxes, directional arrows, clean professional design.`;
1097
+
1098
+ return prompt;
1099
+ }
1100
+
1101
+ /**
1102
+ * Sleep utility for retry backoff
1103
+ *
1104
+ * @param {number} ms - Milliseconds to sleep
1105
+ * @returns {Promise<void>}
1106
+ */
1107
+ function sleep(ms) {
1108
+ return new Promise((resolve) => setTimeout(resolve, ms));
1109
+ }
1110
+
1111
+ /**
1112
+ * Get service information
1113
+ *
1114
+ * @param {string} service - Service name
1115
+ * @returns {Object} Service information
1116
+ */
1117
+ export function getServiceInfo(service) {
1118
+ const info = {
1119
+ gemini: {
1120
+ name: "Gemini 3.0 Pro Image",
1121
+ nickname: "Nano Banana",
1122
+ speed: "Fast",
1123
+ cost: "$0.02/image",
1124
+ quality: "Good",
1125
+ bestFor: "Quick hero images, high volume",
1126
+ provider: "Google Gemini API (API Key)",
1127
+ model: GEMINI_IMAGE_MODEL,
1128
+ },
1129
+ imagen: {
1130
+ name: "Imagen 3",
1131
+ nickname: "Premium Quality",
1132
+ speed: "Medium",
1133
+ cost: "$0.03/image",
1134
+ quality: "Excellent",
1135
+ bestFor: "Premium hero images, photorealistic",
1136
+ provider: "Google Gemini API (API Key)",
1137
+ model: GEMINI_IMAGEN_MODEL,
1138
+ },
1139
+ // OpenAI GPT Image Models (SOTA)
1140
+ "gpt-image-1.5": {
1141
+ name: "GPT Image 1.5",
1142
+ nickname: "State-of-the-Art",
1143
+ speed: "Medium",
1144
+ cost: "$0.02-$0.19/image",
1145
+ quality: "Outstanding",
1146
+ bestFor: "Best text rendering, highest quality, transparent backgrounds",
1147
+ provider: "OpenAI",
1148
+ model: "gpt-image-1.5",
1149
+ features: [
1150
+ "Best-in-class text rendering",
1151
+ "Transparency support",
1152
+ "Multiple quality tiers",
1153
+ "WebP/JPEG/PNG output",
1154
+ ],
1155
+ qualityTiers: {
1156
+ low: "$0.02/image (~272 tokens)",
1157
+ medium: "$0.07/image (~1056 tokens)",
1158
+ high: "$0.19/image (~4160 tokens)",
1159
+ },
1160
+ },
1161
+ "gpt-image-1": {
1162
+ name: "GPT Image 1",
1163
+ nickname: "Premium Quality",
1164
+ speed: "Medium",
1165
+ cost: "$0.19/image",
1166
+ quality: "Outstanding",
1167
+ bestFor: "High quality images, text rendering",
1168
+ provider: "OpenAI",
1169
+ model: "gpt-image-1",
1170
+ features: [
1171
+ "Excellent text rendering",
1172
+ "Multiple sizes",
1173
+ "Transparency support",
1174
+ ],
1175
+ },
1176
+ "gpt-image-1-mini": {
1177
+ name: "GPT Image 1 Mini",
1178
+ nickname: "Cost-Effective",
1179
+ speed: "Fast",
1180
+ cost: "$0.02/image",
1181
+ quality: "Good",
1182
+ bestFor: "Quick images, budget-conscious, high volume",
1183
+ provider: "OpenAI",
1184
+ model: "gpt-image-1-mini",
1185
+ features: ["Fast generation", "Low cost", "Good quality"],
1186
+ },
1187
+ "flux2-pro": {
1188
+ name: "FLUX 2 Pro",
1189
+ nickname: "State-of-the-Art",
1190
+ speed: "Fast",
1191
+ cost: "$0.05/image",
1192
+ quality: "Outstanding",
1193
+ bestFor: "Best quality, fastest generation, lowest cost",
1194
+ provider: "Black Forest Labs (Fal.ai)",
1195
+ model: "flux-2/pro",
1196
+ features: [
1197
+ "Multi-reference (up to 10 images)",
1198
+ "Enhanced photorealism",
1199
+ "Complex typography",
1200
+ "UI mockups",
1201
+ ],
1202
+ },
1203
+ "flux2-flex": {
1204
+ name: "FLUX 2 Flex",
1205
+ nickname: "Developer Control",
1206
+ speed: "Medium",
1207
+ cost: "$0.04/image",
1208
+ quality: "Outstanding",
1209
+ bestFor: "Custom parameters, fine-tuned control",
1210
+ provider: "Black Forest Labs (Fal.ai)",
1211
+ model: "flux-2/flex",
1212
+ features: [
1213
+ "Custom inference steps",
1214
+ "Guidance scale control",
1215
+ "Developer-friendly",
1216
+ ],
1217
+ },
1218
+ "flux2-dev": {
1219
+ name: "FLUX 2 Dev",
1220
+ nickname: "Open-Weight",
1221
+ speed: "Fast",
1222
+ cost: "$0.025/image",
1223
+ quality: "Excellent",
1224
+ bestFor: "Developer workflows, local deployment option",
1225
+ provider: "Black Forest Labs (Fal.ai)",
1226
+ model: "flux-2/dev",
1227
+ features: [
1228
+ "32B parameters",
1229
+ "Open-weight model",
1230
+ "Local deployment available",
1231
+ ],
1232
+ },
1233
+ "flux-pro": {
1234
+ name: "FLUX Pro v1.1 Ultra",
1235
+ nickname: "Legacy Premium",
1236
+ speed: "Medium",
1237
+ cost: "$0.06/image",
1238
+ quality: "Outstanding",
1239
+ bestFor: "Premium artistic images (legacy)",
1240
+ provider: "Fal.ai",
1241
+ },
1242
+ "flux-dev": {
1243
+ name: "FLUX Dev",
1244
+ nickname: "Legacy Developer",
1245
+ speed: "Fast",
1246
+ cost: "$0.025/MP",
1247
+ quality: "Excellent",
1248
+ bestFor: "Developer workflows (legacy)",
1249
+ provider: "Fal.ai",
1250
+ },
1251
+ "nano-banana-pro": {
1252
+ name: "Nano Banana Pro",
1253
+ nickname: "Premium Quality",
1254
+ speed: "Fast",
1255
+ cost: "$0.15/image ($0.30 for 4K)",
1256
+ quality: "Excellent",
1257
+ bestFor: "High-quality branded content, detailed illustrations",
1258
+ provider: "Fal.ai",
1259
+ },
1260
+ veo3: {
1261
+ name: "Veo 3",
1262
+ nickname: "Cutting Edge Video",
1263
+ speed: "Slow",
1264
+ cost: "$0.40/second",
1265
+ quality: "Outstanding",
1266
+ bestFor: "Premium video content, latest features",
1267
+ provider: "Google (Fal.ai)",
1268
+ },
1269
+ };
1270
+
1271
+ return info[service] || null;
1272
+ }