myaidev-method 0.2.12 → 0.2.16

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 (32) hide show
  1. package/.env.example +40 -0
  2. package/CHANGELOG.md +96 -0
  3. package/CONTENT_CREATION_GUIDE.md +3399 -0
  4. package/DEVELOPER_USE_CASES.md +2085 -0
  5. package/README.md +209 -2
  6. package/VISUAL_GENERATION_FILE_ORGANIZATION.md +105 -0
  7. package/bin/cli.js +46 -0
  8. package/package.json +18 -3
  9. package/src/lib/asset-management.js +532 -0
  10. package/src/lib/visual-config-utils.js +424 -0
  11. package/src/lib/visual-generation-utils.js +880 -0
  12. package/src/scripts/configure-visual-apis.js +413 -0
  13. package/src/scripts/generate-visual-cli.js +279 -0
  14. package/src/templates/claude/agents/content-production-coordinator.md +111 -0
  15. package/src/templates/claude/agents/content-writer.md +209 -4
  16. package/src/templates/claude/agents/proprietary-content-verifier.md +96 -0
  17. package/src/templates/claude/agents/visual-content-generator.md +520 -0
  18. package/src/templates/claude/commands/myai-content-writer.md +33 -8
  19. package/src/templates/claude/commands/myai-coordinate-content.md +136 -0
  20. package/src/templates/claude/commands/myai-generate-visual.md +318 -0
  21. package/src/templates/codex/commands/myai-generate-visual.md +307 -0
  22. package/src/templates/gemini/commands/myai-generate-visual.md +200 -0
  23. package/.claude/CLAUDE.md +0 -52
  24. package/.claude/agents/content-writer.md +0 -155
  25. package/.claude/agents/wordpress-admin.md +0 -271
  26. package/.claude/commands/myai-configure.md +0 -44
  27. package/.claude/commands/myai-content-writer.md +0 -78
  28. package/.claude/commands/myai-wordpress-publish.md +0 -120
  29. package/.claude/mcp/gutenberg-converter.js +0 -447
  30. package/.claude/mcp/mcp-config.json +0 -184
  31. package/.claude/mcp/wordpress-server-simple.js +0 -182
  32. package/.claude/settings.local.json +0 -12
@@ -0,0 +1,880 @@
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 (direct Google API)
56
+ imagen: 0.03, // Imagen 3 (direct Google API)
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
+ // Fal.ai pricing
64
+ flux_pro: 0.06, // FLUX Pro v1.1 Ultra
65
+ flux_dev: 0.025, // FLUX Dev (per megapixel)
66
+ veo3: 0.40, // Veo 3 (per second)
67
+ gemini_fal: 0.0398, // Gemini via fal.ai (fallback)
68
+ imagen_fal: 0.05 // Imagen via fal.ai (fallback)
69
+ };
70
+
71
+ /**
72
+ * Validate that required API keys are configured
73
+ *
74
+ * @returns {Object} Validation results
75
+ * @returns {boolean} hasGoogle - Google API key is configured
76
+ * @returns {boolean} hasOpenAI - OpenAI API key is configured
77
+ * @returns {boolean} hasAny - At least one API key is configured
78
+ * @returns {Array<string>} availableServices - List of available services
79
+ */
80
+ export function validateAPIKeys() {
81
+ const googleKey = process.env.GOOGLE_API_KEY;
82
+ const openaiKey = process.env.OPENAI_API_KEY;
83
+
84
+ const hasGoogle = !!(googleKey && googleKey.length > 20);
85
+ const hasOpenAI = !!(openaiKey && openaiKey.length > 20);
86
+
87
+ const availableServices = [];
88
+ if (hasGoogle) {
89
+ availableServices.push('gemini', 'imagen', 'veo');
90
+ }
91
+ if (hasOpenAI) {
92
+ availableServices.push('dalle');
93
+ }
94
+
95
+ return {
96
+ hasGoogle,
97
+ hasOpenAI,
98
+ hasAny: hasGoogle || hasOpenAI,
99
+ availableServices
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Estimate cost for image/video generation
105
+ *
106
+ * @param {string} service - Service name (gemini, imagen, dalle, veo)
107
+ * @param {Object} options - Generation options
108
+ * @param {string} options.quality - Quality level (standard, hd)
109
+ * @param {string} options.size - Image size
110
+ * @returns {number} Estimated cost in USD
111
+ */
112
+ export function estimateCost(service, options = {}) {
113
+ const { quality = 'high', size = '1024x1024' } = options;
114
+
115
+ switch (service) {
116
+ case 'gemini':
117
+ return PRICING.gemini;
118
+
119
+ case 'imagen':
120
+ return PRICING.imagen;
121
+
122
+ case 'dalle':
123
+ // GPT-Image-1 quality-based pricing
124
+ if (quality === 'low') {
125
+ return PRICING.dalle_low;
126
+ } else if (quality === 'medium' || quality === 'standard') {
127
+ return PRICING.dalle_medium;
128
+ } else if (quality === 'high' || quality === 'hd') {
129
+ return PRICING.dalle_high;
130
+ }
131
+ return PRICING.dalle_high; // default to high quality
132
+
133
+ case 'veo':
134
+ return PRICING.veo;
135
+
136
+ default:
137
+ return 0;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Select best available service based on preferences
143
+ *
144
+ * @param {string} preferred - Preferred service name
145
+ * @returns {string} Selected service name
146
+ * @throws {Error} If no API keys are configured
147
+ */
148
+ export function selectBestService(preferred = 'gemini') {
149
+ const { availableServices, hasAny } = validateAPIKeys();
150
+
151
+ if (!hasAny) {
152
+ throw new Error('No API keys configured. Run /myai-configure visual to set up image generation.');
153
+ }
154
+
155
+ // Return preferred service if available
156
+ if (availableServices.includes(preferred)) {
157
+ return preferred;
158
+ }
159
+
160
+ // Fallback to first available service
161
+ return availableServices[0];
162
+ }
163
+
164
+ /**
165
+ * Generate image using Google Gemini 2.5 Flash Image ("Nano Banana")
166
+ * Fast and cost-effective image generation
167
+ *
168
+ * @param {string} prompt - Image description
169
+ * @param {Object} options - Generation options
170
+ * @param {number} options.aspectRatio - Aspect ratio (1 for square, 16/9 for wide)
171
+ * @param {number} options.maxRetries - Maximum retry attempts
172
+ * @returns {Promise<Object>} Generated image data
173
+ */
174
+ export async function generateImageGemini(prompt, options = {}) {
175
+ const {
176
+ aspectRatio = 1,
177
+ maxRetries = 3
178
+ } = options;
179
+
180
+ const apiKey = process.env.GOOGLE_API_KEY;
181
+ if (!apiKey) {
182
+ throw new Error('GOOGLE_API_KEY not configured');
183
+ }
184
+
185
+ const endpoint = `${GOOGLE_API_BASE}/models/gemini-2.0-flash-exp:generateContent`;
186
+
187
+ const requestBody = {
188
+ contents: [{
189
+ parts: [{
190
+ text: `Generate an image: ${prompt}\n\nAspect ratio: ${aspectRatio === 16/9 ? '16:9' : '1:1'}\nStyle: Professional, high quality, suitable for article content`
191
+ }]
192
+ }],
193
+ generationConfig: {
194
+ temperature: 0.4,
195
+ topP: 0.95,
196
+ topK: 40
197
+ }
198
+ };
199
+
200
+ let lastError;
201
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
202
+ try {
203
+ const response = await fetch(`${endpoint}?key=${apiKey}`, {
204
+ method: 'POST',
205
+ headers: {
206
+ 'Content-Type': 'application/json'
207
+ },
208
+ body: JSON.stringify(requestBody)
209
+ });
210
+
211
+ if (!response.ok) {
212
+ const errorText = await response.text();
213
+ throw new Error(`Gemini API error: ${response.status} - ${errorText}`);
214
+ }
215
+
216
+ const data = await response.json();
217
+
218
+ // Extract image data from response
219
+ if (data.candidates && data.candidates[0]) {
220
+ const candidate = data.candidates[0];
221
+
222
+ // Gemini returns inline data or content references
223
+ if (candidate.content && candidate.content.parts) {
224
+ for (const part of candidate.content.parts) {
225
+ if (part.inlineData && part.inlineData.data) {
226
+ return {
227
+ data: part.inlineData.data,
228
+ mimeType: part.inlineData.mimeType || 'image/png',
229
+ service: 'gemini',
230
+ cost: PRICING.gemini
231
+ };
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ throw new Error('No image data in Gemini response');
238
+
239
+ } catch (error) {
240
+ lastError = error;
241
+
242
+ if (attempt < maxRetries) {
243
+ const backoff = Math.pow(2, attempt) * 1000;
244
+ console.log(`⚠️ Gemini attempt ${attempt} failed. Retrying in ${backoff/1000}s...`);
245
+ await sleep(backoff);
246
+ }
247
+ }
248
+ }
249
+
250
+ throw lastError;
251
+ }
252
+
253
+ /**
254
+ * Generate image using Google Imagen 4 (via Vertex AI)
255
+ * Premium quality image generation
256
+ *
257
+ * Requires Vertex AI setup:
258
+ * - GOOGLE_CLOUD_PROJECT_ID environment variable
259
+ * - GOOGLE_CLOUD_LOCATION environment variable (default: us-central1)
260
+ * - GOOGLE_APPLICATION_CREDENTIALS pointing to service account key JSON
261
+ *
262
+ * @param {string} prompt - Image description
263
+ * @param {Object} options - Generation options
264
+ * @param {string} options.size - Image size (256x256, 1024x1024)
265
+ * @param {number} options.maxRetries - Maximum retry attempts
266
+ * @returns {Promise<Object>} Generated image data
267
+ */
268
+ export async function generateImageImagen(prompt, options = {}) {
269
+ const {
270
+ size = '1024x1024',
271
+ maxRetries = 3
272
+ } = options;
273
+
274
+ // Vertex AI configuration
275
+ const projectId = process.env.GOOGLE_CLOUD_PROJECT_ID;
276
+ const location = process.env.GOOGLE_CLOUD_LOCATION || 'us-central1';
277
+
278
+ if (!projectId) {
279
+ throw new Error('GOOGLE_CLOUD_PROJECT_ID not configured. Set up Vertex AI credentials.');
280
+ }
281
+
282
+ // Build Vertex AI endpoint
283
+ const endpoint = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/imagen-4.0-generate-001:predict`;
284
+
285
+ const requestBody = {
286
+ instances: [{
287
+ prompt: prompt
288
+ }],
289
+ parameters: {
290
+ sampleCount: 1,
291
+ aspectRatio: size === '1024x1024' ? '1:1' : (size.includes('1792') ? '16:9' : '1:1'),
292
+ safetyFilterLevel: 'block_some',
293
+ personGeneration: 'allow_adult'
294
+ }
295
+ };
296
+
297
+ let lastError;
298
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
299
+ try {
300
+ // Get OAuth2 access token
301
+ const token = await getVertexAIToken();
302
+
303
+ const response = await fetch(endpoint, {
304
+ method: 'POST',
305
+ headers: {
306
+ 'Authorization': `Bearer ${token}`,
307
+ 'Content-Type': 'application/json'
308
+ },
309
+ body: JSON.stringify(requestBody)
310
+ });
311
+
312
+ if (!response.ok) {
313
+ const errorText = await response.text();
314
+ throw new Error(`Imagen API error: ${response.status} - ${errorText}`);
315
+ }
316
+
317
+ const data = await response.json();
318
+
319
+ if (data.predictions && data.predictions[0]) {
320
+ const prediction = data.predictions[0];
321
+
322
+ // Imagen 4 returns base64-encoded image in bytesBase64Encoded
323
+ if (prediction.bytesBase64Encoded) {
324
+ return {
325
+ data: prediction.bytesBase64Encoded,
326
+ mimeType: prediction.mimeType || 'image/png',
327
+ service: 'imagen',
328
+ cost: PRICING.imagen
329
+ };
330
+ }
331
+ }
332
+
333
+ throw new Error('No image data in Imagen response');
334
+
335
+ } catch (error) {
336
+ lastError = error;
337
+
338
+ if (attempt < maxRetries) {
339
+ const backoff = Math.pow(2, attempt) * 1000;
340
+ console.log(`⚠️ Imagen attempt ${attempt} failed. Retrying in ${backoff/1000}s...`);
341
+ await sleep(backoff);
342
+ }
343
+ }
344
+ }
345
+
346
+ throw lastError;
347
+ }
348
+
349
+ /**
350
+ * Generate image using OpenAI DALL-E 3
351
+ * Creative, high-quality image generation
352
+ *
353
+ * @param {string} prompt - Image description
354
+ * @param {Object} options - Generation options
355
+ * @param {string} options.size - Image size (1024x1024, 1024x1792, 1792x1024)
356
+ * @param {string} options.quality - Quality level (standard, hd)
357
+ * @param {string} options.style - Style (vivid, natural)
358
+ * @param {number} options.maxRetries - Maximum retry attempts
359
+ * @returns {Promise<Object>} Generated image data with URL
360
+ */
361
+ export async function generateImageDALLE(prompt, options = {}) {
362
+ const {
363
+ size = '1024x1024',
364
+ quality = 'high', // low, medium, or high
365
+ maxRetries = 3
366
+ } = options;
367
+
368
+ const apiKey = process.env.OPENAI_API_KEY;
369
+ if (!apiKey) {
370
+ throw new Error('OPENAI_API_KEY not configured');
371
+ }
372
+
373
+ const endpoint = `${OPENAI_API_BASE}/images/generations`;
374
+
375
+ // Try GPT-Image-1 first, fall back to DALL-E 3 if not available
376
+ const model = 'dall-e-3'; // Will use gpt-image-1 once org is verified
377
+
378
+ const requestBody = {
379
+ model: model,
380
+ prompt: prompt,
381
+ n: 1,
382
+ size: size,
383
+ ...(model === 'gpt-image-1' ? { quality } : { quality: quality === 'low' ? 'standard' : 'hd' })
384
+ };
385
+
386
+ let lastError;
387
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
388
+ try {
389
+ const response = await fetch(endpoint, {
390
+ method: 'POST',
391
+ headers: {
392
+ 'Content-Type': 'application/json',
393
+ 'Authorization': `Bearer ${apiKey}`
394
+ },
395
+ body: JSON.stringify(requestBody)
396
+ });
397
+
398
+ if (!response.ok) {
399
+ const errorData = await response.json();
400
+ throw new Error(`DALL-E API error: ${response.status} - ${errorData.error?.message || 'Unknown error'}`);
401
+ }
402
+
403
+ const data = await response.json();
404
+
405
+ if (data.data && data.data[0]) {
406
+ const image = data.data[0];
407
+
408
+ return {
409
+ url: image.url,
410
+ revisedPrompt: image.revised_prompt, // DALL-E often revises prompts
411
+ mimeType: 'image/png',
412
+ service: 'dalle',
413
+ cost: estimateCost('dalle', { quality, size })
414
+ };
415
+ }
416
+
417
+ throw new Error('No image data in DALL-E response');
418
+
419
+ } catch (error) {
420
+ lastError = error;
421
+
422
+ if (error.message.includes('rate_limit')) {
423
+ if (attempt < maxRetries) {
424
+ const backoff = Math.pow(2, attempt) * 1000;
425
+ console.log(`⚠️ DALL-E rate limited. Retrying in ${backoff/1000}s...`);
426
+ await sleep(backoff);
427
+ }
428
+ } else {
429
+ throw error; // Don't retry on other errors
430
+ }
431
+ }
432
+ }
433
+
434
+ throw lastError;
435
+ }
436
+
437
+ /**
438
+ * Generate video using Google Veo 2
439
+ * AI video generation from text prompts
440
+ *
441
+ * @param {string} prompt - Video description
442
+ * @param {Object} options - Generation options
443
+ * @param {number} options.duration - Video duration in seconds (max 8)
444
+ * @param {string} options.aspectRatio - Aspect ratio (16:9, 9:16, 1:1)
445
+ * @param {number} options.maxRetries - Maximum retry attempts
446
+ * @returns {Promise<Object>} Generated video data
447
+ */
448
+ export async function generateVideoVeo(prompt, options = {}) {
449
+ const {
450
+ duration = 5,
451
+ aspectRatio = '16:9',
452
+ maxRetries = 3
453
+ } = options;
454
+
455
+ const apiKey = process.env.GOOGLE_API_KEY;
456
+ if (!apiKey) {
457
+ throw new Error('GOOGLE_API_KEY not configured');
458
+ }
459
+
460
+ const endpoint = `${GOOGLE_API_BASE}/models/veo-2.0-generate-001:predict`;
461
+
462
+ const requestBody = {
463
+ instances: [{
464
+ prompt: prompt
465
+ }],
466
+ parameters: {
467
+ duration: Math.min(duration, 8), // Max 8 seconds
468
+ aspectRatio: aspectRatio,
469
+ quality: '720p'
470
+ }
471
+ };
472
+
473
+ let lastError;
474
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
475
+ try {
476
+ const response = await fetch(`${endpoint}?key=${apiKey}`, {
477
+ method: 'POST',
478
+ headers: {
479
+ 'Content-Type': 'application/json'
480
+ },
481
+ body: JSON.stringify(requestBody)
482
+ });
483
+
484
+ if (!response.ok) {
485
+ const errorText = await response.text();
486
+ throw new Error(`Veo API error: ${response.status} - ${errorText}`);
487
+ }
488
+
489
+ const data = await response.json();
490
+
491
+ if (data.predictions && data.predictions[0]) {
492
+ const prediction = data.predictions[0];
493
+
494
+ // Veo returns video data or URL
495
+ if (prediction.videoData || prediction.url) {
496
+ return {
497
+ data: prediction.videoData,
498
+ url: prediction.url,
499
+ mimeType: 'video/mp4',
500
+ service: 'veo',
501
+ cost: PRICING.veo,
502
+ duration: duration
503
+ };
504
+ }
505
+ }
506
+
507
+ throw new Error('No video data in Veo response');
508
+
509
+ } catch (error) {
510
+ lastError = error;
511
+
512
+ if (attempt < maxRetries) {
513
+ const backoff = Math.pow(2, attempt) * 2000; // Longer backoff for video
514
+ console.log(`⚠️ Veo attempt ${attempt} failed. Retrying in ${backoff/1000}s...`);
515
+ await sleep(backoff);
516
+ }
517
+ }
518
+ }
519
+
520
+ throw lastError;
521
+ }
522
+
523
+ /**
524
+ * Generate image using Fal.ai
525
+ * Access to FLUX and other premium models
526
+ *
527
+ * @param {string} prompt - Image description
528
+ * @param {Object} options - Generation options
529
+ * @param {string} options.model - Fal.ai model (flux-pro, flux-dev, nano-banana, imagen-3-fast)
530
+ * @param {string} options.size - Image size
531
+ * @param {number} options.maxRetries - Maximum retry attempts
532
+ * @returns {Promise<Object>} Generated image data
533
+ */
534
+ export async function generateImageFal(prompt, options = {}) {
535
+ const {
536
+ model = 'flux-pro',
537
+ size = '1024x1024',
538
+ maxRetries = 3
539
+ } = options;
540
+
541
+ const apiKey = process.env.FAL_KEY;
542
+ if (!apiKey) {
543
+ throw new Error('FAL_KEY not configured. Get your key from https://fal.ai/dashboard/keys');
544
+ }
545
+
546
+ // Import fal.ai client
547
+ const { fal } = await import('@fal-ai/client');
548
+
549
+ // Configure credentials
550
+ fal.config({ credentials: apiKey });
551
+
552
+ // Map model names to fal.ai endpoints
553
+ const modelMap = {
554
+ 'flux-pro': 'fal-ai/flux-pro/v1.1-ultra',
555
+ 'flux-dev': 'fal-ai/flux/dev',
556
+ 'nano-banana': 'fal-ai/nano-banana',
557
+ 'imagen-3-fast': 'fal-ai/fast-imagen'
558
+ };
559
+
560
+ const endpoint = modelMap[model] || modelMap['flux-pro'];
561
+
562
+ let lastError;
563
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
564
+ try {
565
+ const result = await fal.subscribe(endpoint, {
566
+ input: {
567
+ prompt: prompt,
568
+ image_size: size === '1024x1024' ? 'square' : 'landscape',
569
+ num_images: 1
570
+ },
571
+ logs: false
572
+ });
573
+
574
+ if (result.images && result.images[0]) {
575
+ const image = result.images[0];
576
+
577
+ // Fal.ai returns URL, need to fetch and convert to base64
578
+ const imageResponse = await fetch(image.url);
579
+ const imageBuffer = await imageResponse.arrayBuffer();
580
+ const base64Data = Buffer.from(imageBuffer).toString('base64');
581
+
582
+ return {
583
+ data: base64Data,
584
+ mimeType: image.content_type || 'image/png',
585
+ service: 'fal',
586
+ model: model,
587
+ cost: PRICING[`${model.replace('-', '_')}`] || PRICING.flux_pro
588
+ };
589
+ }
590
+
591
+ throw new Error('No image data in Fal.ai response');
592
+
593
+ } catch (error) {
594
+ lastError = error;
595
+
596
+ if (attempt < maxRetries) {
597
+ const backoff = Math.pow(2, attempt) * 1000;
598
+ console.log(`⚠️ Fal.ai attempt ${attempt} failed. Retrying in ${backoff/1000}s...`);
599
+ await sleep(backoff);
600
+ }
601
+ }
602
+ }
603
+
604
+ throw lastError;
605
+ }
606
+
607
+ /**
608
+ * Generate video using Fal.ai (Veo 3)
609
+ * Latest video generation models
610
+ *
611
+ * @param {string} prompt - Video description
612
+ * @param {Object} options - Generation options
613
+ * @param {string} options.model - Model (veo3, veo3-fast)
614
+ * @param {number} options.duration - Video duration in seconds
615
+ * @param {string} options.aspectRatio - Aspect ratio
616
+ * @param {number} options.maxRetries - Maximum retry attempts
617
+ * @returns {Promise<Object>} Generated video data
618
+ */
619
+ export async function generateVideoFal(prompt, options = {}) {
620
+ const {
621
+ model = 'veo3',
622
+ duration = 5,
623
+ aspectRatio = '16:9',
624
+ maxRetries = 3
625
+ } = options;
626
+
627
+ const apiKey = process.env.FAL_KEY;
628
+ if (!apiKey) {
629
+ throw new Error('FAL_KEY not configured. Get your key from https://fal.ai/dashboard/keys');
630
+ }
631
+
632
+ // Import fal.ai client
633
+ const { fal } = await import('@fal-ai/client');
634
+
635
+ // Configure credentials
636
+ fal.config({ credentials: apiKey });
637
+
638
+ const endpoint = model === 'veo3-fast' ? 'fal-ai/veo3-fast' : 'fal-ai/veo3';
639
+
640
+ let lastError;
641
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
642
+ try {
643
+ const result = await fal.subscribe(endpoint, {
644
+ input: {
645
+ prompt: prompt,
646
+ duration: Math.min(duration, 10),
647
+ aspect_ratio: aspectRatio
648
+ },
649
+ logs: false
650
+ });
651
+
652
+ if (result.video && result.video.url) {
653
+ // Fal.ai returns video URL
654
+ const videoResponse = await fetch(result.video.url);
655
+ const videoBuffer = await videoResponse.arrayBuffer();
656
+ const base64Data = Buffer.from(videoBuffer).toString('base64');
657
+
658
+ return {
659
+ data: base64Data,
660
+ mimeType: 'video/mp4',
661
+ service: 'fal',
662
+ model: model,
663
+ cost: PRICING.veo3 * duration
664
+ };
665
+ }
666
+
667
+ throw new Error('No video data in Fal.ai response');
668
+
669
+ } catch (error) {
670
+ lastError = error;
671
+
672
+ if (attempt < maxRetries) {
673
+ const backoff = Math.pow(2, attempt) * 1000;
674
+ console.log(`⚠️ Fal.ai video attempt ${attempt} failed. Retrying in ${backoff/1000}s...`);
675
+ await sleep(backoff);
676
+ }
677
+ }
678
+ }
679
+
680
+ throw lastError;
681
+ }
682
+
683
+ /**
684
+ * Download image from URL and return buffer
685
+ *
686
+ * @param {string} url - Image URL
687
+ * @returns {Promise<Buffer>} Image buffer
688
+ */
689
+ export async function downloadImage(url) {
690
+ const response = await fetch(url);
691
+
692
+ if (!response.ok) {
693
+ throw new Error(`Failed to download image: ${response.status}`);
694
+ }
695
+
696
+ const arrayBuffer = await response.arrayBuffer();
697
+ return Buffer.from(arrayBuffer);
698
+ }
699
+
700
+ /**
701
+ * Generate image using auto-selected service
702
+ * Automatically selects best available service based on preferences
703
+ *
704
+ * @param {string} prompt - Image description
705
+ * @param {Object} options - Generation options
706
+ * @param {string} options.preferredService - Preferred service (gemini, imagen, dalle)
707
+ * @param {string} options.type - Image type for optimization (hero, illustration, diagram)
708
+ * @returns {Promise<Object>} Generated image data with buffer
709
+ */
710
+ export async function generateImage(prompt, options = {}) {
711
+ const { preferredService, type = 'general', ...serviceOptions } = options;
712
+
713
+ // Select service
714
+ const defaultService = process.env.VISUAL_DEFAULT_SERVICE || 'gemini';
715
+ const service = selectBestService(preferredService || defaultService);
716
+
717
+ console.log(`🎨 Generating ${type} image using ${service}...`);
718
+
719
+ // Enhance prompt based on image type
720
+ const enhancedPrompt = enhancePrompt(prompt, type);
721
+
722
+ // Generate based on service
723
+ let result;
724
+ switch (service) {
725
+ case 'gemini':
726
+ result = await generateImageGemini(enhancedPrompt, serviceOptions);
727
+ break;
728
+
729
+ case 'imagen':
730
+ result = await generateImageImagen(enhancedPrompt, serviceOptions);
731
+ break;
732
+
733
+ case 'dalle':
734
+ result = await generateImageDALLE(enhancedPrompt, serviceOptions);
735
+ break;
736
+
737
+ case 'flux':
738
+ case 'flux-pro':
739
+ case 'flux-dev':
740
+ result = await generateImageFal(enhancedPrompt, { ...serviceOptions, model: service });
741
+ break;
742
+
743
+ default:
744
+ throw new Error(`Unknown service: ${service}`);
745
+ }
746
+
747
+ // Convert to buffer
748
+ let buffer;
749
+ if (result.data) {
750
+ // Base64 encoded data
751
+ buffer = Buffer.from(result.data, 'base64');
752
+ } else if (result.url) {
753
+ // Download from URL
754
+ buffer = await downloadImage(result.url);
755
+ } else {
756
+ throw new Error('No image data or URL in response');
757
+ }
758
+
759
+ return {
760
+ ...result,
761
+ buffer,
762
+ prompt: enhancedPrompt,
763
+ originalPrompt: prompt
764
+ };
765
+ }
766
+
767
+ /**
768
+ * Enhance prompt based on image type
769
+ *
770
+ * @param {string} prompt - Original prompt
771
+ * @param {string} type - Image type (hero, illustration, diagram, screenshot)
772
+ * @returns {string} Enhanced prompt
773
+ */
774
+ function enhancePrompt(prompt, type) {
775
+ const enhancements = {
776
+ hero: 'Professional hero image, high quality, visually striking, suitable for article header:',
777
+ illustration: 'Clean illustration, professional style, clear and informative:',
778
+ diagram: 'Technical diagram, clear labels, professional design, easy to understand:',
779
+ screenshot: 'Professional screenshot, clean interface, high resolution:',
780
+ general: 'High quality image, professional style:'
781
+ };
782
+
783
+ const prefix = enhancements[type] || enhancements.general;
784
+ return `${prefix} ${prompt}`;
785
+ }
786
+
787
+ /**
788
+ * Sleep utility for retry backoff
789
+ *
790
+ * @param {number} ms - Milliseconds to sleep
791
+ * @returns {Promise<void>}
792
+ */
793
+ function sleep(ms) {
794
+ return new Promise(resolve => setTimeout(resolve, ms));
795
+ }
796
+
797
+ /**
798
+ * Get service information
799
+ *
800
+ * @param {string} service - Service name
801
+ * @returns {Object} Service information
802
+ */
803
+ export function getServiceInfo(service) {
804
+ const info = {
805
+ gemini: {
806
+ name: 'Gemini 2.5 Flash Image',
807
+ nickname: 'Nano Banana',
808
+ speed: 'Fast',
809
+ cost: '$0.02/image',
810
+ quality: 'Good',
811
+ bestFor: 'Quick hero images, high volume',
812
+ provider: 'Google AI (Direct)'
813
+ },
814
+ imagen: {
815
+ name: 'Imagen 3',
816
+ nickname: 'Premium Quality',
817
+ speed: 'Medium',
818
+ cost: '$0.03/image',
819
+ quality: 'Excellent',
820
+ bestFor: 'Premium hero images, photorealistic',
821
+ provider: 'Google Vertex AI (Direct)'
822
+ },
823
+ dalle: {
824
+ name: 'DALL-E 3',
825
+ nickname: 'Creative',
826
+ speed: 'Medium',
827
+ cost: '$0.04-0.12/image',
828
+ quality: 'Excellent',
829
+ bestFor: 'Creative illustrations, concept art',
830
+ provider: 'OpenAI (Direct)'
831
+ },
832
+ veo: {
833
+ name: 'Veo 2',
834
+ nickname: 'Video Generation',
835
+ speed: 'Slow',
836
+ cost: '$0.10/video (estimated)',
837
+ quality: 'Good',
838
+ bestFor: 'Product demos, animated diagrams',
839
+ provider: 'Google AI (Direct)'
840
+ },
841
+ flux: {
842
+ name: 'FLUX Pro v1.1 Ultra',
843
+ nickname: 'Premium Artistic',
844
+ speed: 'Medium',
845
+ cost: '$0.06/image',
846
+ quality: 'Outstanding',
847
+ bestFor: 'Premium artistic images, highest quality',
848
+ provider: 'Fal.ai'
849
+ },
850
+ 'flux-pro': {
851
+ name: 'FLUX Pro v1.1 Ultra',
852
+ nickname: 'Premium Artistic',
853
+ speed: 'Medium',
854
+ cost: '$0.06/image',
855
+ quality: 'Outstanding',
856
+ bestFor: 'Premium artistic images, highest quality',
857
+ provider: 'Fal.ai'
858
+ },
859
+ 'flux-dev': {
860
+ name: 'FLUX Dev',
861
+ nickname: 'Developer Friendly',
862
+ speed: 'Fast',
863
+ cost: '$0.025/MP',
864
+ quality: 'Excellent',
865
+ bestFor: 'Developer workflows, rapid iteration',
866
+ provider: 'Fal.ai'
867
+ },
868
+ veo3: {
869
+ name: 'Veo 3',
870
+ nickname: 'Cutting Edge Video',
871
+ speed: 'Slow',
872
+ cost: '$0.40/second',
873
+ quality: 'Outstanding',
874
+ bestFor: 'Premium video content, latest features',
875
+ provider: 'Fal.ai'
876
+ }
877
+ };
878
+
879
+ return info[service] || null;
880
+ }