myaidev-method 0.2.12 → 0.2.15
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/CLAUDE.md +46 -0
- package/.claude/agents/content-production-coordinator.md +111 -0
- package/.claude/agents/proprietary-content-verifier.md +96 -0
- package/.claude/agents/visual-content-generator.md +520 -0
- package/.claude/commands/myai-coordinate-content.md +136 -0
- package/.claude/settings.local.json +3 -2
- package/.env.example +33 -0
- package/CHANGELOG.md +64 -0
- package/CONTENT_CREATION_GUIDE.md +3399 -0
- package/DEVELOPER_USE_CASES.md +2085 -0
- package/README.md +209 -2
- package/VISUAL_GENERATION_FILE_ORGANIZATION.md +105 -0
- package/bin/cli.js +46 -0
- package/package.json +17 -2
- package/src/lib/asset-management.js +532 -0
- package/src/lib/visual-config-utils.js +424 -0
- package/src/lib/visual-generation-utils.js +668 -0
- package/src/scripts/configure-visual-apis.js +413 -0
- package/src/scripts/generate-visual-cli.js +279 -0
- package/src/templates/claude/agents/content-production-coordinator.md +111 -0
- package/src/templates/claude/agents/content-writer.md +209 -4
- package/src/templates/claude/agents/proprietary-content-verifier.md +96 -0
- package/src/templates/claude/agents/visual-content-generator.md +520 -0
- package/src/templates/claude/commands/myai-content-writer.md +33 -8
- package/src/templates/claude/commands/myai-coordinate-content.md +136 -0
- package/src/templates/claude/commands/myai-generate-visual.md +318 -0
- package/src/templates/codex/commands/myai-generate-visual.md +307 -0
- package/src/templates/gemini/commands/myai-generate-visual.md +200 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual Content Generation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides image and video generation capabilities using:
|
|
5
|
+
* - Google Gemini 2.5 Flash Image ("Nano Banana")
|
|
6
|
+
* - Google Imagen 3
|
|
7
|
+
* - OpenAI GPT-Image-1 (GPT-4o Image Generation)
|
|
8
|
+
* - Google Veo 2 (video)
|
|
9
|
+
*
|
|
10
|
+
* Platform support: Claude Code, Gemini CLI, Codex CLI
|
|
11
|
+
*
|
|
12
|
+
* @module visual-generation-utils
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fetch from 'node-fetch';
|
|
16
|
+
import fs from 'fs-extra';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import dotenv from 'dotenv';
|
|
19
|
+
import { GoogleAuth } from 'google-auth-library';
|
|
20
|
+
|
|
21
|
+
dotenv.config();
|
|
22
|
+
|
|
23
|
+
// API Configuration
|
|
24
|
+
const GOOGLE_API_BASE = 'https://generativelanguage.googleapis.com/v1beta';
|
|
25
|
+
const OPENAI_API_BASE = 'https://api.openai.com/v1';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get OAuth2 access token for Vertex AI
|
|
29
|
+
* Uses Google Application Default Credentials (ADC)
|
|
30
|
+
*
|
|
31
|
+
* @returns {Promise<string>} OAuth2 access token
|
|
32
|
+
* @throws {Error} If authentication fails
|
|
33
|
+
*/
|
|
34
|
+
async function getVertexAIToken() {
|
|
35
|
+
try {
|
|
36
|
+
const auth = new GoogleAuth({
|
|
37
|
+
scopes: ['https://www.googleapis.com/auth/cloud-platform']
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const client = await auth.getClient();
|
|
41
|
+
const tokenResponse = await client.getAccessToken();
|
|
42
|
+
|
|
43
|
+
if (!tokenResponse.token) {
|
|
44
|
+
throw new Error('Failed to obtain access token');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return tokenResponse.token;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(`Vertex AI authentication failed: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Pricing (USD per image/video) - GPT-Image-1 pricing
|
|
54
|
+
const PRICING = {
|
|
55
|
+
gemini: 0.02, // Gemini 2.5 Flash Image
|
|
56
|
+
imagen: 0.03, // Imagen 3
|
|
57
|
+
dalle_low: 0.02, // GPT-Image-1 low quality
|
|
58
|
+
dalle_medium: 0.07, // GPT-Image-1 medium quality
|
|
59
|
+
dalle_standard: 0.07, // GPT-Image-1 medium quality (alias for standard)
|
|
60
|
+
dalle_high: 0.19, // GPT-Image-1 high quality
|
|
61
|
+
dalle_hd: 0.19, // GPT-Image-1 high quality (alias for hd)
|
|
62
|
+
veo: 0.10 // Veo 2 (estimated per video)
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate that required API keys are configured
|
|
67
|
+
*
|
|
68
|
+
* @returns {Object} Validation results
|
|
69
|
+
* @returns {boolean} hasGoogle - Google API key is configured
|
|
70
|
+
* @returns {boolean} hasOpenAI - OpenAI API key is configured
|
|
71
|
+
* @returns {boolean} hasAny - At least one API key is configured
|
|
72
|
+
* @returns {Array<string>} availableServices - List of available services
|
|
73
|
+
*/
|
|
74
|
+
export function validateAPIKeys() {
|
|
75
|
+
const googleKey = process.env.GOOGLE_API_KEY;
|
|
76
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
77
|
+
|
|
78
|
+
const hasGoogle = !!(googleKey && googleKey.length > 20);
|
|
79
|
+
const hasOpenAI = !!(openaiKey && openaiKey.length > 20);
|
|
80
|
+
|
|
81
|
+
const availableServices = [];
|
|
82
|
+
if (hasGoogle) {
|
|
83
|
+
availableServices.push('gemini', 'imagen', 'veo');
|
|
84
|
+
}
|
|
85
|
+
if (hasOpenAI) {
|
|
86
|
+
availableServices.push('dalle');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
hasGoogle,
|
|
91
|
+
hasOpenAI,
|
|
92
|
+
hasAny: hasGoogle || hasOpenAI,
|
|
93
|
+
availableServices
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Estimate cost for image/video generation
|
|
99
|
+
*
|
|
100
|
+
* @param {string} service - Service name (gemini, imagen, dalle, veo)
|
|
101
|
+
* @param {Object} options - Generation options
|
|
102
|
+
* @param {string} options.quality - Quality level (standard, hd)
|
|
103
|
+
* @param {string} options.size - Image size
|
|
104
|
+
* @returns {number} Estimated cost in USD
|
|
105
|
+
*/
|
|
106
|
+
export function estimateCost(service, options = {}) {
|
|
107
|
+
const { quality = 'high', size = '1024x1024' } = options;
|
|
108
|
+
|
|
109
|
+
switch (service) {
|
|
110
|
+
case 'gemini':
|
|
111
|
+
return PRICING.gemini;
|
|
112
|
+
|
|
113
|
+
case 'imagen':
|
|
114
|
+
return PRICING.imagen;
|
|
115
|
+
|
|
116
|
+
case 'dalle':
|
|
117
|
+
// GPT-Image-1 quality-based pricing
|
|
118
|
+
if (quality === 'low') {
|
|
119
|
+
return PRICING.dalle_low;
|
|
120
|
+
} else if (quality === 'medium' || quality === 'standard') {
|
|
121
|
+
return PRICING.dalle_medium;
|
|
122
|
+
} else if (quality === 'high' || quality === 'hd') {
|
|
123
|
+
return PRICING.dalle_high;
|
|
124
|
+
}
|
|
125
|
+
return PRICING.dalle_high; // default to high quality
|
|
126
|
+
|
|
127
|
+
case 'veo':
|
|
128
|
+
return PRICING.veo;
|
|
129
|
+
|
|
130
|
+
default:
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Select best available service based on preferences
|
|
137
|
+
*
|
|
138
|
+
* @param {string} preferred - Preferred service name
|
|
139
|
+
* @returns {string} Selected service name
|
|
140
|
+
* @throws {Error} If no API keys are configured
|
|
141
|
+
*/
|
|
142
|
+
export function selectBestService(preferred = 'gemini') {
|
|
143
|
+
const { availableServices, hasAny } = validateAPIKeys();
|
|
144
|
+
|
|
145
|
+
if (!hasAny) {
|
|
146
|
+
throw new Error('No API keys configured. Run /myai-configure visual to set up image generation.');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Return preferred service if available
|
|
150
|
+
if (availableServices.includes(preferred)) {
|
|
151
|
+
return preferred;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Fallback to first available service
|
|
155
|
+
return availableServices[0];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Generate image using Google Gemini 2.5 Flash Image ("Nano Banana")
|
|
160
|
+
* Fast and cost-effective image generation
|
|
161
|
+
*
|
|
162
|
+
* @param {string} prompt - Image description
|
|
163
|
+
* @param {Object} options - Generation options
|
|
164
|
+
* @param {number} options.aspectRatio - Aspect ratio (1 for square, 16/9 for wide)
|
|
165
|
+
* @param {number} options.maxRetries - Maximum retry attempts
|
|
166
|
+
* @returns {Promise<Object>} Generated image data
|
|
167
|
+
*/
|
|
168
|
+
export async function generateImageGemini(prompt, options = {}) {
|
|
169
|
+
const {
|
|
170
|
+
aspectRatio = 1,
|
|
171
|
+
maxRetries = 3
|
|
172
|
+
} = options;
|
|
173
|
+
|
|
174
|
+
const apiKey = process.env.GOOGLE_API_KEY;
|
|
175
|
+
if (!apiKey) {
|
|
176
|
+
throw new Error('GOOGLE_API_KEY not configured');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const endpoint = `${GOOGLE_API_BASE}/models/gemini-2.0-flash-exp:generateContent`;
|
|
180
|
+
|
|
181
|
+
const requestBody = {
|
|
182
|
+
contents: [{
|
|
183
|
+
parts: [{
|
|
184
|
+
text: `Generate an image: ${prompt}\n\nAspect ratio: ${aspectRatio === 16/9 ? '16:9' : '1:1'}\nStyle: Professional, high quality, suitable for article content`
|
|
185
|
+
}]
|
|
186
|
+
}],
|
|
187
|
+
generationConfig: {
|
|
188
|
+
temperature: 0.4,
|
|
189
|
+
topP: 0.95,
|
|
190
|
+
topK: 40
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
let lastError;
|
|
195
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
196
|
+
try {
|
|
197
|
+
const response = await fetch(`${endpoint}?key=${apiKey}`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: {
|
|
200
|
+
'Content-Type': 'application/json'
|
|
201
|
+
},
|
|
202
|
+
body: JSON.stringify(requestBody)
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
const errorText = await response.text();
|
|
207
|
+
throw new Error(`Gemini API error: ${response.status} - ${errorText}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const data = await response.json();
|
|
211
|
+
|
|
212
|
+
// Extract image data from response
|
|
213
|
+
if (data.candidates && data.candidates[0]) {
|
|
214
|
+
const candidate = data.candidates[0];
|
|
215
|
+
|
|
216
|
+
// Gemini returns inline data or content references
|
|
217
|
+
if (candidate.content && candidate.content.parts) {
|
|
218
|
+
for (const part of candidate.content.parts) {
|
|
219
|
+
if (part.inlineData && part.inlineData.data) {
|
|
220
|
+
return {
|
|
221
|
+
data: part.inlineData.data,
|
|
222
|
+
mimeType: part.inlineData.mimeType || 'image/png',
|
|
223
|
+
service: 'gemini',
|
|
224
|
+
cost: PRICING.gemini
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
throw new Error('No image data in Gemini response');
|
|
232
|
+
|
|
233
|
+
} catch (error) {
|
|
234
|
+
lastError = error;
|
|
235
|
+
|
|
236
|
+
if (attempt < maxRetries) {
|
|
237
|
+
const backoff = Math.pow(2, attempt) * 1000;
|
|
238
|
+
console.log(`⚠️ Gemini attempt ${attempt} failed. Retrying in ${backoff/1000}s...`);
|
|
239
|
+
await sleep(backoff);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
throw lastError;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Generate image using Google Imagen 4 (via Vertex AI)
|
|
249
|
+
* Premium quality image generation
|
|
250
|
+
*
|
|
251
|
+
* Requires Vertex AI setup:
|
|
252
|
+
* - GOOGLE_CLOUD_PROJECT_ID environment variable
|
|
253
|
+
* - GOOGLE_CLOUD_LOCATION environment variable (default: us-central1)
|
|
254
|
+
* - GOOGLE_APPLICATION_CREDENTIALS pointing to service account key JSON
|
|
255
|
+
*
|
|
256
|
+
* @param {string} prompt - Image description
|
|
257
|
+
* @param {Object} options - Generation options
|
|
258
|
+
* @param {string} options.size - Image size (256x256, 1024x1024)
|
|
259
|
+
* @param {number} options.maxRetries - Maximum retry attempts
|
|
260
|
+
* @returns {Promise<Object>} Generated image data
|
|
261
|
+
*/
|
|
262
|
+
export async function generateImageImagen(prompt, options = {}) {
|
|
263
|
+
const {
|
|
264
|
+
size = '1024x1024',
|
|
265
|
+
maxRetries = 3
|
|
266
|
+
} = options;
|
|
267
|
+
|
|
268
|
+
// Vertex AI configuration
|
|
269
|
+
const projectId = process.env.GOOGLE_CLOUD_PROJECT_ID;
|
|
270
|
+
const location = process.env.GOOGLE_CLOUD_LOCATION || 'us-central1';
|
|
271
|
+
|
|
272
|
+
if (!projectId) {
|
|
273
|
+
throw new Error('GOOGLE_CLOUD_PROJECT_ID not configured. Set up Vertex AI credentials.');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Build Vertex AI endpoint
|
|
277
|
+
const endpoint = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/imagen-4.0-generate-001:predict`;
|
|
278
|
+
|
|
279
|
+
const requestBody = {
|
|
280
|
+
instances: [{
|
|
281
|
+
prompt: prompt
|
|
282
|
+
}],
|
|
283
|
+
parameters: {
|
|
284
|
+
sampleCount: 1,
|
|
285
|
+
aspectRatio: size === '1024x1024' ? '1:1' : (size.includes('1792') ? '16:9' : '1:1'),
|
|
286
|
+
safetyFilterLevel: 'block_some',
|
|
287
|
+
personGeneration: 'allow_adult'
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
let lastError;
|
|
292
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
293
|
+
try {
|
|
294
|
+
// Get OAuth2 access token
|
|
295
|
+
const token = await getVertexAIToken();
|
|
296
|
+
|
|
297
|
+
const response = await fetch(endpoint, {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
headers: {
|
|
300
|
+
'Authorization': `Bearer ${token}`,
|
|
301
|
+
'Content-Type': 'application/json'
|
|
302
|
+
},
|
|
303
|
+
body: JSON.stringify(requestBody)
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
const errorText = await response.text();
|
|
308
|
+
throw new Error(`Imagen API error: ${response.status} - ${errorText}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const data = await response.json();
|
|
312
|
+
|
|
313
|
+
if (data.predictions && data.predictions[0]) {
|
|
314
|
+
const prediction = data.predictions[0];
|
|
315
|
+
|
|
316
|
+
// Imagen 4 returns base64-encoded image in bytesBase64Encoded
|
|
317
|
+
if (prediction.bytesBase64Encoded) {
|
|
318
|
+
return {
|
|
319
|
+
data: prediction.bytesBase64Encoded,
|
|
320
|
+
mimeType: prediction.mimeType || 'image/png',
|
|
321
|
+
service: 'imagen',
|
|
322
|
+
cost: PRICING.imagen
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
throw new Error('No image data in Imagen response');
|
|
328
|
+
|
|
329
|
+
} catch (error) {
|
|
330
|
+
lastError = error;
|
|
331
|
+
|
|
332
|
+
if (attempt < maxRetries) {
|
|
333
|
+
const backoff = Math.pow(2, attempt) * 1000;
|
|
334
|
+
console.log(`⚠️ Imagen attempt ${attempt} failed. Retrying in ${backoff/1000}s...`);
|
|
335
|
+
await sleep(backoff);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
throw lastError;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Generate image using OpenAI DALL-E 3
|
|
345
|
+
* Creative, high-quality image generation
|
|
346
|
+
*
|
|
347
|
+
* @param {string} prompt - Image description
|
|
348
|
+
* @param {Object} options - Generation options
|
|
349
|
+
* @param {string} options.size - Image size (1024x1024, 1024x1792, 1792x1024)
|
|
350
|
+
* @param {string} options.quality - Quality level (standard, hd)
|
|
351
|
+
* @param {string} options.style - Style (vivid, natural)
|
|
352
|
+
* @param {number} options.maxRetries - Maximum retry attempts
|
|
353
|
+
* @returns {Promise<Object>} Generated image data with URL
|
|
354
|
+
*/
|
|
355
|
+
export async function generateImageDALLE(prompt, options = {}) {
|
|
356
|
+
const {
|
|
357
|
+
size = '1024x1024',
|
|
358
|
+
quality = 'high', // low, medium, or high
|
|
359
|
+
maxRetries = 3
|
|
360
|
+
} = options;
|
|
361
|
+
|
|
362
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
363
|
+
if (!apiKey) {
|
|
364
|
+
throw new Error('OPENAI_API_KEY not configured');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const endpoint = `${OPENAI_API_BASE}/images/generations`;
|
|
368
|
+
|
|
369
|
+
// Try GPT-Image-1 first, fall back to DALL-E 3 if not available
|
|
370
|
+
const model = 'dall-e-3'; // Will use gpt-image-1 once org is verified
|
|
371
|
+
|
|
372
|
+
const requestBody = {
|
|
373
|
+
model: model,
|
|
374
|
+
prompt: prompt,
|
|
375
|
+
n: 1,
|
|
376
|
+
size: size,
|
|
377
|
+
...(model === 'gpt-image-1' ? { quality } : { quality: quality === 'low' ? 'standard' : 'hd' })
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
let lastError;
|
|
381
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
382
|
+
try {
|
|
383
|
+
const response = await fetch(endpoint, {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
headers: {
|
|
386
|
+
'Content-Type': 'application/json',
|
|
387
|
+
'Authorization': `Bearer ${apiKey}`
|
|
388
|
+
},
|
|
389
|
+
body: JSON.stringify(requestBody)
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
if (!response.ok) {
|
|
393
|
+
const errorData = await response.json();
|
|
394
|
+
throw new Error(`DALL-E API error: ${response.status} - ${errorData.error?.message || 'Unknown error'}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const data = await response.json();
|
|
398
|
+
|
|
399
|
+
if (data.data && data.data[0]) {
|
|
400
|
+
const image = data.data[0];
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
url: image.url,
|
|
404
|
+
revisedPrompt: image.revised_prompt, // DALL-E often revises prompts
|
|
405
|
+
mimeType: 'image/png',
|
|
406
|
+
service: 'dalle',
|
|
407
|
+
cost: estimateCost('dalle', { quality, size })
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
throw new Error('No image data in DALL-E response');
|
|
412
|
+
|
|
413
|
+
} catch (error) {
|
|
414
|
+
lastError = error;
|
|
415
|
+
|
|
416
|
+
if (error.message.includes('rate_limit')) {
|
|
417
|
+
if (attempt < maxRetries) {
|
|
418
|
+
const backoff = Math.pow(2, attempt) * 1000;
|
|
419
|
+
console.log(`⚠️ DALL-E rate limited. Retrying in ${backoff/1000}s...`);
|
|
420
|
+
await sleep(backoff);
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
throw error; // Don't retry on other errors
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
throw lastError;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Generate video using Google Veo 2
|
|
433
|
+
* AI video generation from text prompts
|
|
434
|
+
*
|
|
435
|
+
* @param {string} prompt - Video description
|
|
436
|
+
* @param {Object} options - Generation options
|
|
437
|
+
* @param {number} options.duration - Video duration in seconds (max 8)
|
|
438
|
+
* @param {string} options.aspectRatio - Aspect ratio (16:9, 9:16, 1:1)
|
|
439
|
+
* @param {number} options.maxRetries - Maximum retry attempts
|
|
440
|
+
* @returns {Promise<Object>} Generated video data
|
|
441
|
+
*/
|
|
442
|
+
export async function generateVideoVeo(prompt, options = {}) {
|
|
443
|
+
const {
|
|
444
|
+
duration = 5,
|
|
445
|
+
aspectRatio = '16:9',
|
|
446
|
+
maxRetries = 3
|
|
447
|
+
} = options;
|
|
448
|
+
|
|
449
|
+
const apiKey = process.env.GOOGLE_API_KEY;
|
|
450
|
+
if (!apiKey) {
|
|
451
|
+
throw new Error('GOOGLE_API_KEY not configured');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const endpoint = `${GOOGLE_API_BASE}/models/veo-2.0-generate-001:predict`;
|
|
455
|
+
|
|
456
|
+
const requestBody = {
|
|
457
|
+
instances: [{
|
|
458
|
+
prompt: prompt
|
|
459
|
+
}],
|
|
460
|
+
parameters: {
|
|
461
|
+
duration: Math.min(duration, 8), // Max 8 seconds
|
|
462
|
+
aspectRatio: aspectRatio,
|
|
463
|
+
quality: '720p'
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
let lastError;
|
|
468
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
469
|
+
try {
|
|
470
|
+
const response = await fetch(`${endpoint}?key=${apiKey}`, {
|
|
471
|
+
method: 'POST',
|
|
472
|
+
headers: {
|
|
473
|
+
'Content-Type': 'application/json'
|
|
474
|
+
},
|
|
475
|
+
body: JSON.stringify(requestBody)
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
if (!response.ok) {
|
|
479
|
+
const errorText = await response.text();
|
|
480
|
+
throw new Error(`Veo API error: ${response.status} - ${errorText}`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const data = await response.json();
|
|
484
|
+
|
|
485
|
+
if (data.predictions && data.predictions[0]) {
|
|
486
|
+
const prediction = data.predictions[0];
|
|
487
|
+
|
|
488
|
+
// Veo returns video data or URL
|
|
489
|
+
if (prediction.videoData || prediction.url) {
|
|
490
|
+
return {
|
|
491
|
+
data: prediction.videoData,
|
|
492
|
+
url: prediction.url,
|
|
493
|
+
mimeType: 'video/mp4',
|
|
494
|
+
service: 'veo',
|
|
495
|
+
cost: PRICING.veo,
|
|
496
|
+
duration: duration
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
throw new Error('No video data in Veo response');
|
|
502
|
+
|
|
503
|
+
} catch (error) {
|
|
504
|
+
lastError = error;
|
|
505
|
+
|
|
506
|
+
if (attempt < maxRetries) {
|
|
507
|
+
const backoff = Math.pow(2, attempt) * 2000; // Longer backoff for video
|
|
508
|
+
console.log(`⚠️ Veo attempt ${attempt} failed. Retrying in ${backoff/1000}s...`);
|
|
509
|
+
await sleep(backoff);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
throw lastError;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Download image from URL and return buffer
|
|
519
|
+
*
|
|
520
|
+
* @param {string} url - Image URL
|
|
521
|
+
* @returns {Promise<Buffer>} Image buffer
|
|
522
|
+
*/
|
|
523
|
+
export async function downloadImage(url) {
|
|
524
|
+
const response = await fetch(url);
|
|
525
|
+
|
|
526
|
+
if (!response.ok) {
|
|
527
|
+
throw new Error(`Failed to download image: ${response.status}`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
531
|
+
return Buffer.from(arrayBuffer);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Generate image using auto-selected service
|
|
536
|
+
* Automatically selects best available service based on preferences
|
|
537
|
+
*
|
|
538
|
+
* @param {string} prompt - Image description
|
|
539
|
+
* @param {Object} options - Generation options
|
|
540
|
+
* @param {string} options.preferredService - Preferred service (gemini, imagen, dalle)
|
|
541
|
+
* @param {string} options.type - Image type for optimization (hero, illustration, diagram)
|
|
542
|
+
* @returns {Promise<Object>} Generated image data with buffer
|
|
543
|
+
*/
|
|
544
|
+
export async function generateImage(prompt, options = {}) {
|
|
545
|
+
const { preferredService, type = 'general', ...serviceOptions } = options;
|
|
546
|
+
|
|
547
|
+
// Select service
|
|
548
|
+
const defaultService = process.env.VISUAL_DEFAULT_SERVICE || 'gemini';
|
|
549
|
+
const service = selectBestService(preferredService || defaultService);
|
|
550
|
+
|
|
551
|
+
console.log(`🎨 Generating ${type} image using ${service}...`);
|
|
552
|
+
|
|
553
|
+
// Enhance prompt based on image type
|
|
554
|
+
const enhancedPrompt = enhancePrompt(prompt, type);
|
|
555
|
+
|
|
556
|
+
// Generate based on service
|
|
557
|
+
let result;
|
|
558
|
+
switch (service) {
|
|
559
|
+
case 'gemini':
|
|
560
|
+
result = await generateImageGemini(enhancedPrompt, serviceOptions);
|
|
561
|
+
break;
|
|
562
|
+
|
|
563
|
+
case 'imagen':
|
|
564
|
+
result = await generateImageImagen(enhancedPrompt, serviceOptions);
|
|
565
|
+
break;
|
|
566
|
+
|
|
567
|
+
case 'dalle':
|
|
568
|
+
result = await generateImageDALLE(enhancedPrompt, serviceOptions);
|
|
569
|
+
break;
|
|
570
|
+
|
|
571
|
+
default:
|
|
572
|
+
throw new Error(`Unknown service: ${service}`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Convert to buffer
|
|
576
|
+
let buffer;
|
|
577
|
+
if (result.data) {
|
|
578
|
+
// Base64 encoded data
|
|
579
|
+
buffer = Buffer.from(result.data, 'base64');
|
|
580
|
+
} else if (result.url) {
|
|
581
|
+
// Download from URL
|
|
582
|
+
buffer = await downloadImage(result.url);
|
|
583
|
+
} else {
|
|
584
|
+
throw new Error('No image data or URL in response');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
...result,
|
|
589
|
+
buffer,
|
|
590
|
+
prompt: enhancedPrompt,
|
|
591
|
+
originalPrompt: prompt
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Enhance prompt based on image type
|
|
597
|
+
*
|
|
598
|
+
* @param {string} prompt - Original prompt
|
|
599
|
+
* @param {string} type - Image type (hero, illustration, diagram, screenshot)
|
|
600
|
+
* @returns {string} Enhanced prompt
|
|
601
|
+
*/
|
|
602
|
+
function enhancePrompt(prompt, type) {
|
|
603
|
+
const enhancements = {
|
|
604
|
+
hero: 'Professional hero image, high quality, visually striking, suitable for article header:',
|
|
605
|
+
illustration: 'Clean illustration, professional style, clear and informative:',
|
|
606
|
+
diagram: 'Technical diagram, clear labels, professional design, easy to understand:',
|
|
607
|
+
screenshot: 'Professional screenshot, clean interface, high resolution:',
|
|
608
|
+
general: 'High quality image, professional style:'
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const prefix = enhancements[type] || enhancements.general;
|
|
612
|
+
return `${prefix} ${prompt}`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Sleep utility for retry backoff
|
|
617
|
+
*
|
|
618
|
+
* @param {number} ms - Milliseconds to sleep
|
|
619
|
+
* @returns {Promise<void>}
|
|
620
|
+
*/
|
|
621
|
+
function sleep(ms) {
|
|
622
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Get service information
|
|
627
|
+
*
|
|
628
|
+
* @param {string} service - Service name
|
|
629
|
+
* @returns {Object} Service information
|
|
630
|
+
*/
|
|
631
|
+
export function getServiceInfo(service) {
|
|
632
|
+
const info = {
|
|
633
|
+
gemini: {
|
|
634
|
+
name: 'Gemini 2.5 Flash Image',
|
|
635
|
+
nickname: 'Nano Banana',
|
|
636
|
+
speed: 'Fast',
|
|
637
|
+
cost: '$0.02/image',
|
|
638
|
+
quality: 'Good',
|
|
639
|
+
bestFor: 'Quick hero images, high volume'
|
|
640
|
+
},
|
|
641
|
+
imagen: {
|
|
642
|
+
name: 'Imagen 3',
|
|
643
|
+
nickname: 'Premium Quality',
|
|
644
|
+
speed: 'Medium',
|
|
645
|
+
cost: '$0.03/image',
|
|
646
|
+
quality: 'Excellent',
|
|
647
|
+
bestFor: 'Premium hero images, photorealistic'
|
|
648
|
+
},
|
|
649
|
+
dalle: {
|
|
650
|
+
name: 'DALL-E 3',
|
|
651
|
+
nickname: 'Creative',
|
|
652
|
+
speed: 'Medium',
|
|
653
|
+
cost: '$0.04-0.12/image',
|
|
654
|
+
quality: 'Excellent',
|
|
655
|
+
bestFor: 'Creative illustrations, concept art'
|
|
656
|
+
},
|
|
657
|
+
veo: {
|
|
658
|
+
name: 'Veo 2',
|
|
659
|
+
nickname: 'Video Generation',
|
|
660
|
+
speed: 'Slow',
|
|
661
|
+
cost: '$0.10/video (estimated)',
|
|
662
|
+
quality: 'Good',
|
|
663
|
+
bestFor: 'Product demos, animated diagrams'
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
return info[service] || null;
|
|
668
|
+
}
|