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.
- package/.claude-plugin/plugin.json +0 -1
- package/.env.example +5 -4
- package/CHANGELOG.md +2 -2
- package/CONTENT_CREATION_GUIDE.md +489 -3211
- package/DEVELOPER_USE_CASES.md +1 -1
- package/MODULAR_INSTALLATION.md +2 -2
- package/README.md +39 -33
- package/TECHNICAL_ARCHITECTURE.md +1 -1
- package/USER_GUIDE.md +242 -190
- package/agents/content-editor-agent.md +90 -0
- package/agents/content-planner-agent.md +97 -0
- package/agents/content-research-agent.md +62 -0
- package/agents/content-seo-agent.md +101 -0
- package/agents/content-writer-agent.md +69 -0
- package/agents/infographic-analyzer-agent.md +63 -0
- package/agents/infographic-designer-agent.md +72 -0
- package/bin/cli.js +776 -422
- package/{content-rules.example.md → content-rules-example.md} +2 -2
- package/dist/mcp/health-check.js +82 -68
- package/dist/mcp/mcp-config.json +8 -0
- package/dist/mcp/openstack-server.js +1746 -1262
- package/dist/server/.tsbuildinfo +1 -1
- package/extension.json +21 -4
- package/package.json +181 -184
- package/skills/company-config/SKILL.md +133 -0
- package/skills/configure/SKILL.md +1 -1
- package/skills/myai-configurator/SKILL.md +77 -0
- package/skills/myai-configurator/content-creation-configurator/SKILL.md +516 -0
- package/skills/myai-configurator/content-maintenance-configurator/SKILL.md +397 -0
- package/skills/myai-content-enrichment/SKILL.md +114 -0
- package/skills/myai-content-ideation/SKILL.md +288 -0
- package/skills/myai-content-ideation/evals/evals.json +182 -0
- package/skills/myai-content-production-coordinator/SKILL.md +946 -0
- package/skills/{content-rules-setup → myai-content-rules-setup}/SKILL.md +1 -1
- package/skills/{content-verifier → myai-content-verifier}/SKILL.md +1 -1
- package/skills/myai-content-writer/SKILL.md +333 -0
- package/skills/{infographic → myai-infographic}/SKILL.md +1 -1
- package/skills/myai-proprietary-content-verifier/SKILL.md +175 -0
- package/skills/myai-proprietary-content-verifier/evals/evals.json +36 -0
- package/skills/myai-skill-builder/SKILL.md +699 -0
- package/skills/myai-skill-builder/agents/analyzer-agent.md +137 -0
- package/skills/myai-skill-builder/agents/comparator-agent.md +77 -0
- package/skills/myai-skill-builder/agents/grader-agent.md +103 -0
- package/skills/myai-skill-builder/assets/eval_review.html +131 -0
- package/skills/myai-skill-builder/references/schemas.md +211 -0
- package/skills/myai-skill-builder/scripts/aggregate_benchmark.py +190 -0
- package/skills/myai-skill-builder/scripts/generate_review.py +381 -0
- package/skills/myai-skill-builder/scripts/package_skill.py +91 -0
- package/skills/myai-skill-builder/scripts/run_eval.py +105 -0
- package/skills/myai-skill-builder/scripts/run_loop.py +211 -0
- package/skills/myai-skill-builder/scripts/utils.py +123 -0
- package/skills/myai-visual-generator/SKILL.md +125 -0
- package/skills/myai-visual-generator/evals/evals.json +155 -0
- package/skills/myai-visual-generator/references/infographic-pipeline.md +73 -0
- package/skills/myai-visual-generator/references/research-visuals.md +57 -0
- package/skills/myai-visual-generator/references/services.md +89 -0
- package/skills/myai-visual-generator/scripts/visual-generation-utils.js +1272 -0
- package/skills/myaidev-figma/SKILL.md +212 -0
- package/skills/myaidev-figma/capture.js +133 -0
- package/skills/myaidev-figma/crawl.js +130 -0
- package/skills/myaidev-figma-configure/SKILL.md +130 -0
- package/skills/openstack-manager/SKILL.md +1 -1
- package/skills/payloadcms-publisher/SKILL.md +141 -77
- package/skills/payloadcms-publisher/references/field-mapping.md +142 -0
- package/skills/payloadcms-publisher/references/lexical-format.md +97 -0
- package/skills/security-auditor/SKILL.md +1 -1
- package/src/cli/commands/addon.js +105 -7
- package/src/config/workflows.js +172 -228
- package/src/lib/ascii-banner.js +197 -182
- package/src/lib/{content-coordinator.js → content-production-coordinator.js} +649 -459
- package/src/lib/installation-detector.js +93 -59
- package/src/lib/payloadcms-utils.js +285 -510
- package/src/lib/workflow-installer.js +55 -0
- package/src/mcp/health-check.js +82 -68
- package/src/mcp/openstack-server.js +1746 -1262
- package/src/scripts/configure-visual-apis.js +224 -173
- package/src/scripts/configure-wordpress-mcp.js +96 -66
- package/src/scripts/init/install.js +109 -85
- package/src/scripts/init-project.js +138 -67
- package/src/scripts/utils/write-content.js +67 -52
- package/src/scripts/wordpress/publish-to-wordpress.js +128 -128
- package/src/templates/claude/CLAUDE.md +19 -12
- package/hooks/hooks.json +0 -26
- package/skills/content-coordinator/SKILL.md +0 -130
- package/skills/content-enrichment/SKILL.md +0 -80
- package/skills/content-writer/SKILL.md +0 -285
- package/skills/skill-builder/SKILL.md +0 -417
- package/skills/visual-generator/SKILL.md +0 -140
- /package/skills/{content-writer → myai-content-writer}/agents/editor-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/planner-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/research-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/seo-agent.md +0 -0
- /package/skills/{content-writer → myai-content-writer}/agents/visual-planner-agent.md +0 -0
- /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
|
+
}
|