opencode-nanobanana 0.1.0

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 (179) hide show
  1. package/.ralph-events.json +151 -0
  2. package/.ralph-last-branch +1 -0
  3. package/.ralph-monitor-state.json +7 -0
  4. package/.ralph-monitor.pid +1 -0
  5. package/.ralph-timing.json +26 -0
  6. package/README.md +708 -0
  7. package/dist/index.d.ts +18 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +21 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/platforms/android.d.ts +94 -0
  12. package/dist/platforms/android.d.ts.map +1 -0
  13. package/dist/platforms/android.js +123 -0
  14. package/dist/platforms/android.js.map +1 -0
  15. package/dist/platforms/ios.d.ts +51 -0
  16. package/dist/platforms/ios.d.ts.map +1 -0
  17. package/dist/platforms/ios.js +149 -0
  18. package/dist/platforms/ios.js.map +1 -0
  19. package/dist/platforms/macos.d.ts +33 -0
  20. package/dist/platforms/macos.d.ts.map +1 -0
  21. package/dist/platforms/macos.js +50 -0
  22. package/dist/platforms/macos.js.map +1 -0
  23. package/dist/platforms/watchos.d.ts +36 -0
  24. package/dist/platforms/watchos.d.ts.map +1 -0
  25. package/dist/platforms/watchos.js +113 -0
  26. package/dist/platforms/watchos.js.map +1 -0
  27. package/dist/platforms/web.d.ts +64 -0
  28. package/dist/platforms/web.d.ts.map +1 -0
  29. package/dist/platforms/web.js +96 -0
  30. package/dist/platforms/web.js.map +1 -0
  31. package/dist/providers/gemini.d.ts +41 -0
  32. package/dist/providers/gemini.d.ts.map +1 -0
  33. package/dist/providers/gemini.js +177 -0
  34. package/dist/providers/gemini.js.map +1 -0
  35. package/dist/tools/analyze/compare.d.ts +12 -0
  36. package/dist/tools/analyze/compare.d.ts.map +1 -0
  37. package/dist/tools/analyze/compare.js +83 -0
  38. package/dist/tools/analyze/compare.js.map +1 -0
  39. package/dist/tools/analyze/mockup.d.ts +12 -0
  40. package/dist/tools/analyze/mockup.d.ts.map +1 -0
  41. package/dist/tools/analyze/mockup.js +88 -0
  42. package/dist/tools/analyze/mockup.js.map +1 -0
  43. package/dist/tools/analyze/screenshot.d.ts +12 -0
  44. package/dist/tools/analyze/screenshot.d.ts.map +1 -0
  45. package/dist/tools/analyze/screenshot.js +61 -0
  46. package/dist/tools/analyze/screenshot.js.map +1 -0
  47. package/dist/tools/app-assets/app-icon.d.ts +9 -0
  48. package/dist/tools/app-assets/app-icon.d.ts.map +1 -0
  49. package/dist/tools/app-assets/app-icon.js +133 -0
  50. package/dist/tools/app-assets/app-icon.js.map +1 -0
  51. package/dist/tools/app-assets/device-mockup.d.ts +9 -0
  52. package/dist/tools/app-assets/device-mockup.d.ts.map +1 -0
  53. package/dist/tools/app-assets/device-mockup.js +139 -0
  54. package/dist/tools/app-assets/device-mockup.js.map +1 -0
  55. package/dist/tools/app-assets/launch-images.d.ts +3 -0
  56. package/dist/tools/app-assets/launch-images.d.ts.map +1 -0
  57. package/dist/tools/app-assets/launch-images.js +171 -0
  58. package/dist/tools/app-assets/launch-images.js.map +1 -0
  59. package/dist/tools/app-assets/resize-devices.d.ts +14 -0
  60. package/dist/tools/app-assets/resize-devices.d.ts.map +1 -0
  61. package/dist/tools/app-assets/resize-devices.js +296 -0
  62. package/dist/tools/app-assets/resize-devices.js.map +1 -0
  63. package/dist/tools/app-assets/screenshots.d.ts +14 -0
  64. package/dist/tools/app-assets/screenshots.d.ts.map +1 -0
  65. package/dist/tools/app-assets/screenshots.js +186 -0
  66. package/dist/tools/app-assets/screenshots.js.map +1 -0
  67. package/dist/tools/core/edit-image.d.ts +12 -0
  68. package/dist/tools/core/edit-image.d.ts.map +1 -0
  69. package/dist/tools/core/edit-image.js +102 -0
  70. package/dist/tools/core/edit-image.js.map +1 -0
  71. package/dist/tools/core/generate-image.d.ts +12 -0
  72. package/dist/tools/core/generate-image.d.ts.map +1 -0
  73. package/dist/tools/core/generate-image.js +96 -0
  74. package/dist/tools/core/generate-image.js.map +1 -0
  75. package/dist/tools/core/restore-image.d.ts +12 -0
  76. package/dist/tools/core/restore-image.d.ts.map +1 -0
  77. package/dist/tools/core/restore-image.js +104 -0
  78. package/dist/tools/core/restore-image.js.map +1 -0
  79. package/dist/tools/design/mockup-to-code.d.ts +3 -0
  80. package/dist/tools/design/mockup-to-code.d.ts.map +1 -0
  81. package/dist/tools/design/mockup-to-code.js +311 -0
  82. package/dist/tools/design/mockup-to-code.js.map +1 -0
  83. package/dist/tools/design/sketch-to-code.d.ts +3 -0
  84. package/dist/tools/design/sketch-to-code.d.ts.map +1 -0
  85. package/dist/tools/design/sketch-to-code.js +325 -0
  86. package/dist/tools/design/sketch-to-code.js.map +1 -0
  87. package/dist/tools/docs/architecture-diagram.d.ts +12 -0
  88. package/dist/tools/docs/architecture-diagram.d.ts.map +1 -0
  89. package/dist/tools/docs/architecture-diagram.js +179 -0
  90. package/dist/tools/docs/architecture-diagram.js.map +1 -0
  91. package/dist/tools/docs/readme-banner.d.ts +6 -0
  92. package/dist/tools/docs/readme-banner.d.ts.map +1 -0
  93. package/dist/tools/docs/readme-banner.js +108 -0
  94. package/dist/tools/docs/readme-banner.js.map +1 -0
  95. package/dist/tools/docs/sequence-diagram.d.ts +12 -0
  96. package/dist/tools/docs/sequence-diagram.d.ts.map +1 -0
  97. package/dist/tools/docs/sequence-diagram.js +161 -0
  98. package/dist/tools/docs/sequence-diagram.js.map +1 -0
  99. package/dist/tools/docs/social-preview.d.ts +11 -0
  100. package/dist/tools/docs/social-preview.d.ts.map +1 -0
  101. package/dist/tools/docs/social-preview.js +111 -0
  102. package/dist/tools/docs/social-preview.js.map +1 -0
  103. package/dist/tools/video/extend-video.d.ts +14 -0
  104. package/dist/tools/video/extend-video.d.ts.map +1 -0
  105. package/dist/tools/video/extend-video.js +39 -0
  106. package/dist/tools/video/extend-video.js.map +1 -0
  107. package/dist/tools/video/generate-video.d.ts +14 -0
  108. package/dist/tools/video/generate-video.d.ts.map +1 -0
  109. package/dist/tools/video/generate-video.js +39 -0
  110. package/dist/tools/video/generate-video.js.map +1 -0
  111. package/dist/tools/video/image-to-video.d.ts +15 -0
  112. package/dist/tools/video/image-to-video.d.ts.map +1 -0
  113. package/dist/tools/video/image-to-video.js +42 -0
  114. package/dist/tools/video/image-to-video.js.map +1 -0
  115. package/dist/tools/video/storyboard-video.d.ts +91 -0
  116. package/dist/tools/video/storyboard-video.d.ts.map +1 -0
  117. package/dist/tools/video/storyboard-video.js +230 -0
  118. package/dist/tools/video/storyboard-video.js.map +1 -0
  119. package/dist/utils/ffmpeg.d.ts +30 -0
  120. package/dist/utils/ffmpeg.d.ts.map +1 -0
  121. package/dist/utils/ffmpeg.js +205 -0
  122. package/dist/utils/ffmpeg.js.map +1 -0
  123. package/dist/utils/file-handler.d.ts +7 -0
  124. package/dist/utils/file-handler.d.ts.map +1 -0
  125. package/dist/utils/file-handler.js +10 -0
  126. package/dist/utils/file-handler.js.map +1 -0
  127. package/dist/utils/image-processing.d.ts +7 -0
  128. package/dist/utils/image-processing.d.ts.map +1 -0
  129. package/dist/utils/image-processing.js +10 -0
  130. package/dist/utils/image-processing.js.map +1 -0
  131. package/docs/PLUGIN-VERIFICATION.md +182 -0
  132. package/logs/notifications.jsonl +46 -0
  133. package/package.json +61 -0
  134. package/prd.json +216 -0
  135. package/progress.txt +145 -0
  136. package/ralph-report.html +297 -0
  137. package/src/index.ts +23 -0
  138. package/src/platforms/android/.gitkeep +0 -0
  139. package/src/platforms/ios/.gitkeep +0 -0
  140. package/src/platforms/web/.gitkeep +0 -0
  141. package/src/providers/.gitkeep +0 -0
  142. package/src/providers/gemini.ts +288 -0
  143. package/src/tools/core/.gitkeep +0 -0
  144. package/src/tools/platform/.gitkeep +0 -0
  145. package/src/tools/video/extend-video.ts +71 -0
  146. package/src/tools/video/generate-video.ts +70 -0
  147. package/src/tools/video/image-to-video.ts +76 -0
  148. package/src/tools/video/storyboard-video.ts +325 -0
  149. package/src/utils/.gitkeep +0 -0
  150. package/src/utils/ffmpeg.ts +266 -0
  151. package/src/utils/file-handler.ts +10 -0
  152. package/src/utils/image-processing.ts +10 -0
  153. package/templates/.gitkeep +0 -0
  154. package/test-analyze-screenshot.ts +50 -0
  155. package/test-app-icons.ts +55 -0
  156. package/test-cat-sunset.ts +30 -0
  157. package/test-full-plugin.ts +88 -0
  158. package/test-icon-gen.ts +30 -0
  159. package/test-output/test-edit.png +0 -0
  160. package/test-output/test-generate.png +0 -0
  161. package/test-output/test-video.mp4 +0 -0
  162. package/test-plugin-load.js +45 -0
  163. package/test-princess-emma-continue.ts +35 -0
  164. package/test-princess-emma-full.ts +38 -0
  165. package/test-princess-emma-short.ts +32 -0
  166. package/test-princess-emma-with-reference.ts +34 -0
  167. package/test-princess-emma.ts +38 -0
  168. package/test-product-ad.ts +66 -0
  169. package/test-ralph-droid.ts +30 -0
  170. package/test-social-preview.ts +61 -0
  171. package/test-veo31-live.ts +187 -0
  172. package/test-video-gen.ts +40 -0
  173. package/test-video-veo.ts +73 -0
  174. package/test-zurich-video.ts +64 -0
  175. package/tests/.gitkeep +0 -0
  176. package/tests/providers/gemini.test.ts +388 -0
  177. package/tests/utils/ffmpeg.test.ts +328 -0
  178. package/tests/video/storyboard.test.ts +469 -0
  179. package/tsconfig.json +25 -0
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Storyboard Video Generation Tool
3
+ *
4
+ * Generates a multi-scene video by creating individual scenes in parallel
5
+ * and stitching them together with transitions using FFmpeg.
6
+ */
7
+
8
+ import { GeminiProvider, type ReferenceImage } from '../../providers/gemini.js';
9
+ import {
10
+ checkFfmpegInstalled,
11
+ concatenateVideos,
12
+ addAudioTrack,
13
+ type ConcatenateOptions,
14
+ } from '../../utils/ffmpeg.js';
15
+ import { writeFile, unlink, readFile } from 'fs/promises';
16
+ import { join } from 'path';
17
+ import { tmpdir } from 'os';
18
+
19
+ export interface StoryboardVideoOptions {
20
+ scenes: string[];
21
+ style?: string;
22
+ characterDescription?: string;
23
+ referenceImages?: string[];
24
+ aspectRatio?: '16:9' | '9:16';
25
+ transition?: 'cut' | 'crossfade' | 'fade';
26
+ transitionDuration?: number;
27
+ backgroundMusic?: string;
28
+ musicVolume?: number;
29
+ outputPath?: string;
30
+ apiKey: string;
31
+ }
32
+
33
+ export interface StoryboardVideoResult {
34
+ /** Path to the final stitched video */
35
+ videoPath: string;
36
+ /** Total generation time in milliseconds */
37
+ totalTime: number;
38
+ /** Per-scene generation times */
39
+ sceneTimes: Array<{ scene: number; time: number }>;
40
+ /** Number of scenes successfully generated */
41
+ successCount: number;
42
+ /** Number of scenes that failed */
43
+ failureCount: number;
44
+ }
45
+
46
+ /**
47
+ * Generate a multi-scene storyboard video
48
+ *
49
+ * This function generates multiple video scenes in parallel and stitches them
50
+ * together with transitions to create a cohesive video narrative.
51
+ *
52
+ * @param options - Configuration options for the storyboard video
53
+ * @returns Result object with video path and timing information
54
+ *
55
+ * @example
56
+ * Basic usage:
57
+ * ```typescript
58
+ * const result = await generateStoryboardVideo({
59
+ * apiKey: 'your-api-key',
60
+ * scenes: [
61
+ * 'A serene mountain landscape at sunrise',
62
+ * 'A hiker reaching the summit',
63
+ * 'Panoramic view from the peak'
64
+ * ],
65
+ * style: 'cinematic',
66
+ * transition: 'crossfade',
67
+ * outputPath: './mountain-journey.mp4'
68
+ * });
69
+ * ```
70
+ *
71
+ * @example
72
+ * With character consistency:
73
+ * ```typescript
74
+ * const result = await generateStoryboardVideo({
75
+ * apiKey: 'your-api-key',
76
+ * characterDescription: 'A young woman with long brown hair wearing a red jacket',
77
+ * referenceImages: ['./character-ref.jpg'],
78
+ * scenes: [
79
+ * 'Walking through a forest',
80
+ * 'Discovering a hidden waterfall',
81
+ * 'Setting up camp at sunset'
82
+ * ],
83
+ * style: 'cinematic',
84
+ * transition: 'crossfade'
85
+ * });
86
+ * ```
87
+ *
88
+ * @example
89
+ * With background music:
90
+ * ```typescript
91
+ * const result = await generateStoryboardVideo({
92
+ * apiKey: 'your-api-key',
93
+ * scenes: ['Scene 1', 'Scene 2', 'Scene 3'],
94
+ * generateAudio: true, // Native Veo audio (default)
95
+ * backgroundMusic: './music.mp3', // Add background music
96
+ * musicVolume: 0.3, // 30% volume for background music
97
+ * transition: 'crossfade'
98
+ * });
99
+ * ```
100
+ */
101
+ export async function generateStoryboardVideo(
102
+ options: StoryboardVideoOptions
103
+ ): Promise<StoryboardVideoResult> {
104
+ const startTime = Date.now();
105
+
106
+ // Validate inputs
107
+ if (!options.scenes || options.scenes.length === 0) {
108
+ throw new Error('At least one scene is required');
109
+ }
110
+
111
+ // Check FFmpeg availability
112
+ const ffmpegAvailable = await checkFfmpegInstalled();
113
+ if (!ffmpegAvailable) {
114
+ throw new Error(
115
+ 'FFmpeg is not installed or not available in PATH. Please install FFmpeg to use this tool.'
116
+ );
117
+ }
118
+
119
+ // Initialize provider
120
+ const provider = new GeminiProvider(options.apiKey);
121
+
122
+ const {
123
+ scenes,
124
+ style,
125
+ characterDescription,
126
+ referenceImages,
127
+ aspectRatio = '16:9',
128
+ transition = 'crossfade',
129
+ transitionDuration = 0.5,
130
+ backgroundMusic,
131
+ musicVolume = 0.3,
132
+ outputPath = join(tmpdir(), `storyboard-${Date.now()}.mp4`),
133
+ } = options;
134
+
135
+ // Load reference images if provided
136
+ let loadedReferences: ReferenceImage[] | undefined;
137
+ if (referenceImages && referenceImages.length > 0) {
138
+ if (referenceImages.length > 3) {
139
+ throw new Error('Maximum of 3 reference images allowed');
140
+ }
141
+
142
+ console.log(`📸 Loading ${referenceImages.length} reference image(s)...`);
143
+ loadedReferences = await Promise.all(
144
+ referenceImages.map(async (imagePath, index) => {
145
+ const buffer = await readFile(imagePath);
146
+ return {
147
+ buffer,
148
+ description: `Reference image ${index + 1} for character/scene consistency`,
149
+ };
150
+ })
151
+ );
152
+ }
153
+
154
+ console.log(`\n🎬 Starting storyboard generation with ${scenes.length} scene(s)`);
155
+ console.log(`⚙️ Configuration:`);
156
+ console.log(` - Aspect ratio: ${aspectRatio}`);
157
+ console.log(` - Transition: ${transition} (${transitionDuration}s)`);
158
+ console.log(` - Audio: native (Veo 3.0)${backgroundMusic ? ' + background music' : ''}`);
159
+
160
+ if (characterDescription) {
161
+ console.log(`👤 Using character description: "${characterDescription}"`);
162
+ }
163
+ if (loadedReferences) {
164
+ console.log(`🖼️ Using ${loadedReferences.length} reference image(s) for consistency`);
165
+ }
166
+
167
+ console.log(`\n📹 Generating scenes sequentially (to avoid rate limits)...`);
168
+
169
+ type SceneResult = {
170
+ index: number;
171
+ path: string | null;
172
+ time: number;
173
+ success: boolean;
174
+ error?: string;
175
+ };
176
+
177
+ const sceneResults: SceneResult[] = [];
178
+
179
+ for (let index = 0; index < scenes.length; index++) {
180
+ const sceneDescription = scenes[index]!;
181
+ const sceneStartTime = Date.now();
182
+ console.log(` [${index + 1}/${scenes.length}] Generating: "${sceneDescription.slice(0, 50)}${sceneDescription.length > 50 ? '...' : ''}"`);
183
+
184
+ try {
185
+ let prompt = sceneDescription;
186
+
187
+ if (characterDescription) {
188
+ prompt = `${characterDescription}. ${prompt}`;
189
+ }
190
+
191
+ if (style) {
192
+ prompt = `${style} style: ${prompt}`;
193
+ }
194
+
195
+ let result;
196
+ if (loadedReferences && loadedReferences.length > 0) {
197
+ result = await provider.generateVideoWithReferences(
198
+ prompt,
199
+ loadedReferences,
200
+ {
201
+ aspectRatio,
202
+ resolution: '720p',
203
+ duration: 8,
204
+ numberOfVideos: 1,
205
+ }
206
+ );
207
+ } else {
208
+ result = await provider.generateVideo(prompt, {
209
+ aspectRatio,
210
+ resolution: '720p',
211
+ duration: 8,
212
+ numberOfVideos: 1,
213
+ });
214
+ }
215
+
216
+ const tempPath = join(tmpdir(), `scene-${index}-${Date.now()}.mp4`);
217
+ await writeFile(tempPath, result.buffer);
218
+
219
+ const sceneTime = Date.now() - sceneStartTime;
220
+ console.log(` ✅ [${index + 1}/${scenes.length}] Completed in ${(sceneTime / 1000).toFixed(1)}s`);
221
+
222
+ sceneResults.push({
223
+ index,
224
+ path: tempPath,
225
+ time: sceneTime,
226
+ success: true,
227
+ });
228
+ } catch (error) {
229
+ const sceneTime = Date.now() - sceneStartTime;
230
+ console.error(` ❌ [${index + 1}/${scenes.length}] Failed after ${(sceneTime / 1000).toFixed(1)}s:`, error);
231
+
232
+ sceneResults.push({
233
+ index,
234
+ path: null,
235
+ time: sceneTime,
236
+ success: false,
237
+ error: error instanceof Error ? error.message : String(error),
238
+ });
239
+ }
240
+ }
241
+
242
+ // Filter successful scenes
243
+ const successfulScenes = sceneResults.filter(
244
+ (result): result is { index: number; path: string; time: number; success: true } =>
245
+ result.success && result.path !== null
246
+ );
247
+
248
+ if (successfulScenes.length === 0) {
249
+ throw new Error('All scenes failed to generate. No video to stitch.');
250
+ }
251
+
252
+ if (successfulScenes.length < sceneResults.length) {
253
+ console.warn(
254
+ `⚠️ ${sceneResults.length - successfulScenes.length} scene(s) failed. Continuing with ${successfulScenes.length} successful scene(s).`
255
+ );
256
+ }
257
+
258
+ // Sort by original index to maintain scene order
259
+ successfulScenes.sort((a, b) => a.index - b.index);
260
+ const videoPaths = successfulScenes.map((s) => s.path);
261
+
262
+ // Stitch videos together
263
+ console.log(`\n🎞️ Stitching ${videoPaths.length} scene(s) together...`);
264
+ console.log(` - Transition: ${transition}`);
265
+ console.log(` - Duration: ${transitionDuration}s`);
266
+
267
+ const concatenateOptions: ConcatenateOptions = {
268
+ transition,
269
+ transitionDuration,
270
+ };
271
+
272
+ // Determine final output path based on whether we need to add background music
273
+ const stitchedVideoPath = backgroundMusic
274
+ ? join(tmpdir(), `stitched-${Date.now()}.mp4`)
275
+ : outputPath;
276
+
277
+ try {
278
+ await concatenateVideos(videoPaths, stitchedVideoPath, concatenateOptions);
279
+
280
+ // Add background music if provided
281
+ if (backgroundMusic) {
282
+ console.log(`\n🎵 Adding background music...`);
283
+ console.log(` - Volume: ${(musicVolume * 100).toFixed(0)}%`);
284
+ await addAudioTrack(stitchedVideoPath, backgroundMusic, outputPath, musicVolume);
285
+ console.log(` ✅ Audio mixing complete`);
286
+ // Clean up temporary stitched video
287
+ await unlink(stitchedVideoPath).catch(() => {});
288
+ }
289
+ } finally {
290
+ // Clean up temporary scene files
291
+ for (const videoPath of videoPaths) {
292
+ try {
293
+ await unlink(videoPath);
294
+ } catch {
295
+ // Ignore cleanup errors
296
+ }
297
+ }
298
+ }
299
+
300
+ const totalTime = Date.now() - startTime;
301
+
302
+ // Calculate average scene generation time
303
+ const avgSceneTime = successfulScenes.length > 0
304
+ ? successfulScenes.reduce((sum, s) => sum + s.time, 0) / successfulScenes.length
305
+ : 0;
306
+
307
+ console.log(`\n✨ Storyboard generation complete!`);
308
+ console.log(`📊 Summary:`);
309
+ console.log(` - Total time: ${(totalTime / 1000).toFixed(1)}s`);
310
+ console.log(` - Scenes generated: ${successfulScenes.length}/${sceneResults.length}`);
311
+ console.log(` - Average scene time: ${(avgSceneTime / 1000).toFixed(1)}s`);
312
+ console.log(` - Success rate: ${((successfulScenes.length / sceneResults.length) * 100).toFixed(0)}%`);
313
+ console.log(`📁 Output: ${outputPath}\n`);
314
+
315
+ return {
316
+ videoPath: outputPath,
317
+ totalTime,
318
+ sceneTimes: sceneResults.map((r) => ({
319
+ scene: r.index + 1,
320
+ time: r.time,
321
+ })),
322
+ successCount: successfulScenes.length,
323
+ failureCount: sceneResults.length - successfulScenes.length,
324
+ };
325
+ }
File without changes
@@ -0,0 +1,266 @@
1
+ /**
2
+ * FFmpeg Utility Module
3
+ *
4
+ * Provides video processing operations using FFmpeg
5
+ */
6
+
7
+ import { exec } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import { existsSync } from 'fs';
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ export interface ConcatenateOptions {
14
+ transition?: 'cut' | 'crossfade' | 'fade';
15
+ transitionDuration?: number;
16
+ }
17
+
18
+ /**
19
+ * Check if FFmpeg is installed and available
20
+ */
21
+ export async function checkFfmpegInstalled(): Promise<boolean> {
22
+ try {
23
+ await execAsync('ffmpeg -version');
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Get the duration of a video in seconds
32
+ */
33
+ export async function getVideoDuration(videoPath: string): Promise<number> {
34
+ if (!existsSync(videoPath)) {
35
+ throw new Error(`Video file not found: ${videoPath}`);
36
+ }
37
+
38
+ try {
39
+ const { stdout } = await execAsync(
40
+ `ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoPath}"`
41
+ );
42
+ const duration = parseFloat(stdout.trim());
43
+ if (isNaN(duration)) {
44
+ throw new Error(`Failed to parse video duration: ${stdout}`);
45
+ }
46
+ return duration;
47
+ } catch (error) {
48
+ throw new Error(`Failed to get video duration: ${error instanceof Error ? error.message : String(error)}`);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Trim a video from startTime for the specified duration
54
+ */
55
+ export async function trimVideo(
56
+ videoPath: string,
57
+ startTime: number,
58
+ duration: number,
59
+ outputPath: string
60
+ ): Promise<void> {
61
+ if (!existsSync(videoPath)) {
62
+ throw new Error(`Video file not found: ${videoPath}`);
63
+ }
64
+
65
+ if (startTime < 0 || duration <= 0) {
66
+ throw new Error(`Invalid trim parameters: startTime=${startTime}, duration=${duration}`);
67
+ }
68
+
69
+ try {
70
+ await execAsync(
71
+ `ffmpeg -i "${videoPath}" -ss ${startTime} -t ${duration} -c copy "${outputPath}" -y`
72
+ );
73
+ } catch (error) {
74
+ throw new Error(`Failed to trim video: ${error instanceof Error ? error.message : String(error)}`);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Add an audio track to a video
80
+ */
81
+ export async function addAudioTrack(
82
+ videoPath: string,
83
+ audioPath: string,
84
+ outputPath: string,
85
+ volume: number = 1.0
86
+ ): Promise<void> {
87
+ if (!existsSync(videoPath)) {
88
+ throw new Error(`Video file not found: ${videoPath}`);
89
+ }
90
+
91
+ if (!existsSync(audioPath)) {
92
+ throw new Error(`Audio file not found: ${audioPath}`);
93
+ }
94
+
95
+ if (volume < 0 || volume > 1) {
96
+ throw new Error(`Volume must be between 0 and 1, got: ${volume}`);
97
+ }
98
+
99
+ try {
100
+ // Mix video audio with background audio, adjust volume of background
101
+ await execAsync(
102
+ `ffmpeg -i "${videoPath}" -i "${audioPath}" -filter_complex "[1:a]volume=${volume}[a1];[0:a][a1]amix=inputs=2:duration=first[aout]" -map 0:v -map "[aout]" -c:v copy -c:a aac "${outputPath}" -y`
103
+ );
104
+ } catch (error) {
105
+ throw new Error(`Failed to add audio track: ${error instanceof Error ? error.message : String(error)}`);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Concatenate multiple videos with transitions
111
+ */
112
+ export async function concatenateVideos(
113
+ videoPaths: string[],
114
+ outputPath: string,
115
+ options: ConcatenateOptions = {}
116
+ ): Promise<void> {
117
+ if (videoPaths.length === 0) {
118
+ throw new Error('No videos provided for concatenation');
119
+ }
120
+
121
+ // Check all input files exist
122
+ for (const videoPath of videoPaths) {
123
+ if (!existsSync(videoPath)) {
124
+ throw new Error(`Video file not found: ${videoPath}`);
125
+ }
126
+ }
127
+
128
+ const { transition = 'crossfade', transitionDuration = 0.5 } = options;
129
+
130
+ try {
131
+ if (transition === 'cut') {
132
+ // Simple concatenation without transitions
133
+ await concatenateWithCut(videoPaths, outputPath);
134
+ } else if (transition === 'crossfade') {
135
+ // Crossfade transition between videos
136
+ await concatenateWithCrossfade(videoPaths, outputPath, transitionDuration);
137
+ } else if (transition === 'fade') {
138
+ // Fade to black transition between videos
139
+ await concatenateWithFade(videoPaths, outputPath, transitionDuration);
140
+ } else {
141
+ throw new Error(`Unknown transition type: ${transition}`);
142
+ }
143
+ } catch (error) {
144
+ throw new Error(`Failed to concatenate videos: ${error instanceof Error ? error.message : String(error)}`);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Concatenate videos with simple cut (no transition)
150
+ */
151
+ async function concatenateWithCut(videoPaths: string[], outputPath: string): Promise<void> {
152
+ // Create a temporary concat file
153
+ const concatListPath = outputPath + '.concat.txt';
154
+ const fs = await import('fs/promises');
155
+
156
+ const concatContent = videoPaths.map(path => `file '${path}'`).join('\n');
157
+ await fs.writeFile(concatListPath, concatContent);
158
+
159
+ try {
160
+ await execAsync(
161
+ `ffmpeg -f concat -safe 0 -i "${concatListPath}" -c copy "${outputPath}" -y`
162
+ );
163
+ } finally {
164
+ // Clean up concat file
165
+ await fs.unlink(concatListPath).catch(() => {});
166
+ }
167
+ }
168
+
169
+ async function concatenateWithCrossfade(
170
+ videoPaths: string[],
171
+ outputPath: string,
172
+ transitionDuration: number
173
+ ): Promise<void> {
174
+ if (videoPaths.length === 1) {
175
+ const fs = await import('fs/promises');
176
+ const firstVideo = videoPaths[0];
177
+ if (!firstVideo) {
178
+ throw new Error('No video path provided');
179
+ }
180
+ await fs.copyFile(firstVideo, outputPath);
181
+ return;
182
+ }
183
+
184
+ const durations: number[] = [];
185
+ for (const videoPath of videoPaths) {
186
+ const dur = await getVideoDuration(videoPath);
187
+ durations.push(dur);
188
+ }
189
+
190
+ let filterComplex = '';
191
+ let audioFilterComplex = '';
192
+ let currentVideoLabel = '[0:v]';
193
+ let currentAudioLabel = '[0:a]';
194
+ let cumulativeOffset = durations[0]! - transitionDuration;
195
+
196
+ for (let i = 1; i < videoPaths.length; i++) {
197
+ const isLast = i === videoPaths.length - 1;
198
+ const nextVideoLabel = isLast ? '[outv]' : `[v${i}]`;
199
+ const nextAudioLabel = isLast ? '[outa]' : `[a${i}]`;
200
+
201
+ filterComplex += `${currentVideoLabel}[${i}:v]xfade=transition=fade:duration=${transitionDuration}:offset=${cumulativeOffset}${nextVideoLabel};`;
202
+ audioFilterComplex += `${currentAudioLabel}[${i}:a]acrossfade=d=${transitionDuration}${nextAudioLabel};`;
203
+
204
+ currentVideoLabel = nextVideoLabel;
205
+ currentAudioLabel = nextAudioLabel;
206
+
207
+ if (!isLast) {
208
+ cumulativeOffset += durations[i]! - transitionDuration;
209
+ }
210
+ }
211
+
212
+ const fullFilter = filterComplex + audioFilterComplex.slice(0, -1);
213
+ const inputs = videoPaths.map(path => `-i "${path}"`).join(' ');
214
+
215
+ await execAsync(
216
+ `ffmpeg ${inputs} -filter_complex "${fullFilter}" -map "[outv]" -map "[outa]" "${outputPath}" -y`
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Concatenate videos with fade to black transition
222
+ */
223
+ async function concatenateWithFade(
224
+ videoPaths: string[],
225
+ outputPath: string,
226
+ duration: number
227
+ ): Promise<void> {
228
+ if (videoPaths.length === 1) {
229
+ // No transition needed for single video
230
+ const fs = await import('fs/promises');
231
+ const firstVideo = videoPaths[0];
232
+ if (!firstVideo) {
233
+ throw new Error('No video path provided');
234
+ }
235
+ await fs.copyFile(firstVideo, outputPath);
236
+ return;
237
+ }
238
+
239
+ // For fade transition, we fade out each clip and fade in the next
240
+ // This is complex with FFmpeg, using a simpler concatenation approach
241
+ const inputs = videoPaths.map(path => `-i "${path}"`).join(' ');
242
+
243
+ // Build filter to fade out and fade in
244
+ let filterComplex = '';
245
+ for (let i = 0; i < videoPaths.length; i++) {
246
+ if (i < videoPaths.length - 1) {
247
+ // Fade out at the end of each video except the last
248
+ filterComplex += `[${i}:v]fade=t=out:st=0:d=${duration}[v${i}out];`;
249
+ // Fade in at the start of next video
250
+ filterComplex += `[${i + 1}:v]fade=t=in:st=0:d=${duration}[v${i + 1}in];`;
251
+ }
252
+ }
253
+
254
+ // Concatenate the faded segments
255
+ const concatInputs = videoPaths.map((_, i) => {
256
+ if (i === 0) return '[v0out]';
257
+ if (i === videoPaths.length - 1) return `[v${i}in]`;
258
+ return `[v${i}out][v${i}in]`;
259
+ }).join('');
260
+
261
+ filterComplex += `${concatInputs}concat=n=${videoPaths.length}:v=1:a=0[outv]`;
262
+
263
+ await execAsync(
264
+ `ffmpeg ${inputs} -filter_complex "${filterComplex}" -map "[outv]" "${outputPath}" -y`
265
+ );
266
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * File Handler Utilities
3
+ *
4
+ * Utilities for handling file operations
5
+ */
6
+
7
+ export function ensureDirectory(_path: string): Promise<void> {
8
+ // Placeholder - will be implemented when needed
9
+ return Promise.resolve();
10
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Image Processing Utilities
3
+ *
4
+ * Utilities for processing and manipulating images
5
+ */
6
+
7
+ export function resizeImage(buffer: Buffer, _width: number, _height: number): Promise<Buffer> {
8
+ // Placeholder - will be implemented when needed
9
+ return Promise.resolve(buffer);
10
+ }
File without changes
@@ -0,0 +1,50 @@
1
+ import { GeminiProvider } from './src/providers/gemini.js';
2
+ import * as fs from 'fs/promises';
3
+
4
+ async function analyzeScreenshot() {
5
+ console.log('🔍 Analyzing Screenshot...\n');
6
+
7
+ const provider = new GeminiProvider();
8
+ const imagePath = '/Volumes/DevHub_ext/factory/toolbox/opencode-nanobanana/generated-assets/screenshots/IMG_3619.jpg';
9
+
10
+ console.log(`📁 Image: ${imagePath}\n`);
11
+
12
+ const imageBuffer = await fs.readFile(imagePath);
13
+ console.log(`📦 Size: ${(imageBuffer.length / 1024).toFixed(1)}KB\n`);
14
+
15
+ const question = `Analyze this UI screenshot in detail. Please provide:
16
+
17
+ 1. **OVERVIEW**: What app/screen is this? What is its purpose?
18
+
19
+ 2. **WHAT'S GOOD** ✅:
20
+ - Layout and composition
21
+ - Typography and readability
22
+ - Color scheme and contrast
23
+ - Visual hierarchy
24
+ - User experience elements
25
+
26
+ 3. **WHAT NEEDS IMPROVEMENT** ❌:
27
+ - Accessibility issues
28
+ - Design inconsistencies
29
+ - Usability problems
30
+ - Visual clutter or confusion
31
+ - Missing elements
32
+
33
+ 4. **SPECIFIC RECOMMENDATIONS**:
34
+ - Actionable suggestions to improve the UI
35
+
36
+ Be specific and reference actual elements you see in the screenshot.`;
37
+
38
+ console.log('⏳ Analyzing with AI vision...\n');
39
+
40
+ const startTime = Date.now();
41
+ const analysis = await provider.analyzeImage(imageBuffer, question);
42
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
43
+
44
+ console.log(`✅ Analysis complete in ${duration}s\n`);
45
+ console.log('━'.repeat(60));
46
+ console.log('\n' + analysis + '\n');
47
+ console.log('━'.repeat(60));
48
+ }
49
+
50
+ analyzeScreenshot().catch(console.error);