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,469 @@
1
+ /**
2
+ * Storyboard Video Integration Tests
3
+ *
4
+ * Integration tests for multi-scene storyboard video generation
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ import { generateStoryboardVideo } from '../../src/tools/video/storyboard-video.js';
9
+ import * as ffmpeg from '../../src/utils/ffmpeg.js';
10
+ import { GeminiProvider } from '../../src/providers/gemini.js';
11
+ import * as fs from 'fs/promises';
12
+ import { existsSync } from 'fs';
13
+
14
+ // Mock dependencies
15
+ vi.mock('../../src/utils/ffmpeg.js', () => ({
16
+ checkFfmpegInstalled: vi.fn(),
17
+ concatenateVideos: vi.fn(),
18
+ addAudioTrack: vi.fn(),
19
+ }));
20
+
21
+ vi.mock('fs', () => ({
22
+ existsSync: vi.fn(),
23
+ }));
24
+
25
+ vi.mock('fs/promises', () => ({
26
+ writeFile: vi.fn(),
27
+ unlink: vi.fn(),
28
+ readFile: vi.fn(),
29
+ }));
30
+
31
+ vi.mock('../../src/providers/gemini.js', () => {
32
+ const mockProvider = {
33
+ generateVideo: vi.fn(),
34
+ generateVideoWithReferences: vi.fn(),
35
+ };
36
+
37
+ return {
38
+ GeminiProvider: vi.fn(() => mockProvider),
39
+ };
40
+ });
41
+
42
+ describe('Storyboard Video Integration Tests', () => {
43
+ let mockProvider: any;
44
+
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+
48
+ // Setup default mocks - cast to vi.Mock to access mock methods
49
+ (ffmpeg.checkFfmpegInstalled as any).mockResolvedValue(true);
50
+ (ffmpeg.concatenateVideos as any).mockResolvedValue();
51
+ (ffmpeg.addAudioTrack as any).mockResolvedValue();
52
+ (existsSync as any).mockReturnValue(true);
53
+ (fs.writeFile as any).mockResolvedValue();
54
+ (fs.unlink as any).mockResolvedValue();
55
+ (fs.readFile as any).mockResolvedValue(Buffer.from('fake-image-data'));
56
+
57
+ // Get mocked provider instance
58
+ mockProvider = new GeminiProvider('test-api-key');
59
+
60
+ // Setup default video generation responses
61
+ mockProvider.generateVideo.mockResolvedValue({
62
+ buffer: Buffer.from('fake-video-data'),
63
+ });
64
+
65
+ mockProvider.generateVideoWithReferences.mockResolvedValue({
66
+ buffer: Buffer.from('fake-video-data'),
67
+ });
68
+ });
69
+
70
+ describe('Multi-scene generation with mocked Veo API', () => {
71
+ it('should generate multiple scenes in parallel', async () => {
72
+ const options = {
73
+ apiKey: 'test-api-key',
74
+ scenes: [
75
+ 'A serene mountain landscape',
76
+ 'A hiker on a trail',
77
+ 'A campfire at dusk',
78
+ ],
79
+ };
80
+
81
+ const result = await generateStoryboardVideo(options);
82
+
83
+ // Verify all scenes were generated
84
+ expect(mockProvider.generateVideo).toHaveBeenCalledTimes(3);
85
+
86
+ // Verify scenes were written to temp files
87
+ expect(fs.writeFile).toHaveBeenCalledTimes(3);
88
+
89
+ // Verify result structure
90
+ expect(result).toMatchObject({
91
+ videoPath: expect.any(String),
92
+ totalTime: expect.any(Number),
93
+ sceneTimes: expect.arrayContaining([
94
+ { scene: 1, time: expect.any(Number) },
95
+ { scene: 2, time: expect.any(Number) },
96
+ { scene: 3, time: expect.any(Number) },
97
+ ]),
98
+ successCount: 3,
99
+ failureCount: 0,
100
+ });
101
+ });
102
+
103
+ it('should apply style prefix to all scenes', async () => {
104
+ const options = {
105
+ apiKey: 'test-api-key',
106
+ scenes: ['Mountain landscape', 'Hiker on trail'],
107
+ style: 'cinematic',
108
+ };
109
+
110
+ await generateStoryboardVideo(options);
111
+
112
+ // Verify style prefix was added to prompts
113
+ expect(mockProvider.generateVideo).toHaveBeenCalledWith(
114
+ 'cinematic style: Mountain landscape',
115
+ expect.any(Object)
116
+ );
117
+ expect(mockProvider.generateVideo).toHaveBeenCalledWith(
118
+ 'cinematic style: Hiker on trail',
119
+ expect.any(Object)
120
+ );
121
+ });
122
+
123
+ it('should pass correct options to video generation', async () => {
124
+ const options = {
125
+ apiKey: 'test-api-key',
126
+ scenes: ['Test scene'],
127
+ aspectRatio: '9:16' as const,
128
+ generateAudio: false,
129
+ };
130
+
131
+ await generateStoryboardVideo(options);
132
+
133
+ expect(mockProvider.generateVideo).toHaveBeenCalledWith(
134
+ 'Test scene',
135
+ expect.objectContaining({
136
+ aspectRatio: '9:16',
137
+ resolution: '720p',
138
+ duration: 8,
139
+ generateAudio: false,
140
+ numberOfVideos: 1,
141
+ })
142
+ );
143
+ });
144
+ });
145
+
146
+ describe('FFmpeg stitching with mocked commands', () => {
147
+ it('should call concatenateVideos with correct parameters', async () => {
148
+ const options = {
149
+ apiKey: 'test-api-key',
150
+ scenes: ['Scene 1', 'Scene 2'],
151
+ transition: 'crossfade' as const,
152
+ transitionDuration: 1.0,
153
+ outputPath: '/output/test-video.mp4',
154
+ };
155
+
156
+ await generateStoryboardVideo(options);
157
+
158
+ expect(ffmpeg.concatenateVideos).toHaveBeenCalledWith(
159
+ expect.arrayContaining([
160
+ expect.stringContaining('scene-0-'),
161
+ expect.stringContaining('scene-1-'),
162
+ ]),
163
+ '/output/test-video.mp4',
164
+ {
165
+ transition: 'crossfade',
166
+ transitionDuration: 1.0,
167
+ }
168
+ );
169
+ });
170
+
171
+ it('should clean up temporary scene files after stitching', async () => {
172
+ const options = {
173
+ apiKey: 'test-api-key',
174
+ scenes: ['Scene 1', 'Scene 2'],
175
+ };
176
+
177
+ await generateStoryboardVideo(options);
178
+
179
+ // Verify unlink was called for each scene file
180
+ expect(fs.unlink).toHaveBeenCalledTimes(2);
181
+ });
182
+
183
+ it('should handle background music mixing', async () => {
184
+ const options = {
185
+ apiKey: 'test-api-key',
186
+ scenes: ['Scene 1'],
187
+ backgroundMusic: '/path/to/music.mp3',
188
+ musicVolume: 0.5,
189
+ outputPath: '/output/final.mp4',
190
+ };
191
+
192
+ await generateStoryboardVideo(options);
193
+
194
+ // Verify stitching happened to temp file first
195
+ expect(ffmpeg.concatenateVideos).toHaveBeenCalledWith(
196
+ expect.any(Array),
197
+ expect.stringContaining('stitched-'),
198
+ expect.any(Object)
199
+ );
200
+
201
+ // Verify audio was added to final output
202
+ expect(ffmpeg.addAudioTrack).toHaveBeenCalledWith(
203
+ expect.stringContaining('stitched-'),
204
+ '/path/to/music.mp3',
205
+ '/output/final.mp4',
206
+ 0.5
207
+ );
208
+ });
209
+ });
210
+
211
+ describe('Transition options (cut, crossfade, fade)', () => {
212
+ it('should use cut transition when specified', async () => {
213
+ const options = {
214
+ apiKey: 'test-api-key',
215
+ scenes: ['Scene 1', 'Scene 2'],
216
+ transition: 'cut' as const,
217
+ };
218
+
219
+ await generateStoryboardVideo(options);
220
+
221
+ expect(ffmpeg.concatenateVideos).toHaveBeenCalledWith(
222
+ expect.any(Array),
223
+ expect.any(String),
224
+ expect.objectContaining({
225
+ transition: 'cut',
226
+ })
227
+ );
228
+ });
229
+
230
+ it('should use crossfade transition by default', async () => {
231
+ const options = {
232
+ apiKey: 'test-api-key',
233
+ scenes: ['Scene 1', 'Scene 2'],
234
+ };
235
+
236
+ await generateStoryboardVideo(options);
237
+
238
+ expect(ffmpeg.concatenateVideos).toHaveBeenCalledWith(
239
+ expect.any(Array),
240
+ expect.any(String),
241
+ expect.objectContaining({
242
+ transition: 'crossfade',
243
+ })
244
+ );
245
+ });
246
+
247
+ it('should use fade transition when specified', async () => {
248
+ const options = {
249
+ apiKey: 'test-api-key',
250
+ scenes: ['Scene 1', 'Scene 2'],
251
+ transition: 'fade' as const,
252
+ transitionDuration: 0.75,
253
+ };
254
+
255
+ await generateStoryboardVideo(options);
256
+
257
+ expect(ffmpeg.concatenateVideos).toHaveBeenCalledWith(
258
+ expect.any(Array),
259
+ expect.any(String),
260
+ expect.objectContaining({
261
+ transition: 'fade',
262
+ transitionDuration: 0.75,
263
+ })
264
+ );
265
+ });
266
+ });
267
+
268
+ describe('Character consistency with style prefix', () => {
269
+ it('should prepend character description to each scene', async () => {
270
+ const options = {
271
+ apiKey: 'test-api-key',
272
+ scenes: ['Walking in forest', 'Discovering waterfall'],
273
+ characterDescription: 'A young woman with red jacket',
274
+ };
275
+
276
+ await generateStoryboardVideo(options);
277
+
278
+ expect(mockProvider.generateVideo).toHaveBeenCalledWith(
279
+ 'A young woman with red jacket. Walking in forest',
280
+ expect.any(Object)
281
+ );
282
+ expect(mockProvider.generateVideo).toHaveBeenCalledWith(
283
+ 'A young woman with red jacket. Discovering waterfall',
284
+ expect.any(Object)
285
+ );
286
+ });
287
+
288
+ it('should use reference images for character consistency', async () => {
289
+ const options = {
290
+ apiKey: 'test-api-key',
291
+ scenes: ['Scene 1', 'Scene 2'],
292
+ referenceImages: ['/path/to/ref1.jpg', '/path/to/ref2.jpg'],
293
+ };
294
+
295
+ await generateStoryboardVideo(options);
296
+
297
+ // Verify reference images were loaded
298
+ expect(fs.readFile).toHaveBeenCalledWith('/path/to/ref1.jpg');
299
+ expect(fs.readFile).toHaveBeenCalledWith('/path/to/ref2.jpg');
300
+
301
+ // Verify generateVideoWithReferences was used
302
+ expect(mockProvider.generateVideoWithReferences).toHaveBeenCalledTimes(2);
303
+ expect(mockProvider.generateVideoWithReferences).toHaveBeenCalledWith(
304
+ 'Scene 1',
305
+ expect.arrayContaining([
306
+ expect.objectContaining({
307
+ buffer: expect.any(Buffer),
308
+ description: expect.stringContaining('Reference image'),
309
+ }),
310
+ ]),
311
+ expect.any(Object)
312
+ );
313
+ });
314
+
315
+ it('should combine character description, style, and scene', async () => {
316
+ const options = {
317
+ apiKey: 'test-api-key',
318
+ scenes: ['Walking through city'],
319
+ characterDescription: 'Detective in trench coat',
320
+ style: 'noir',
321
+ };
322
+
323
+ await generateStoryboardVideo(options);
324
+
325
+ expect(mockProvider.generateVideo).toHaveBeenCalledWith(
326
+ 'noir style: Detective in trench coat. Walking through city',
327
+ expect.any(Object)
328
+ );
329
+ });
330
+
331
+ it('should reject more than 3 reference images', async () => {
332
+ const options = {
333
+ apiKey: 'test-api-key',
334
+ scenes: ['Scene 1'],
335
+ referenceImages: [
336
+ '/ref1.jpg',
337
+ '/ref2.jpg',
338
+ '/ref3.jpg',
339
+ '/ref4.jpg',
340
+ ],
341
+ };
342
+
343
+ await expect(generateStoryboardVideo(options)).rejects.toThrow(
344
+ 'Maximum of 3 reference images allowed'
345
+ );
346
+ });
347
+ });
348
+
349
+ describe('Error handling for failed scenes', () => {
350
+ it('should continue with successful scenes when some fail', async () => {
351
+ // Mock one scene to fail
352
+ mockProvider.generateVideo
353
+ .mockResolvedValueOnce({ buffer: Buffer.from('video1') })
354
+ .mockRejectedValueOnce(new Error('Generation failed'))
355
+ .mockResolvedValueOnce({ buffer: Buffer.from('video3') });
356
+
357
+ const options = {
358
+ apiKey: 'test-api-key',
359
+ scenes: ['Scene 1', 'Scene 2', 'Scene 3'],
360
+ };
361
+
362
+ const result = await generateStoryboardVideo(options);
363
+
364
+ // Should have 2 successful and 1 failed
365
+ expect(result.successCount).toBe(2);
366
+ expect(result.failureCount).toBe(1);
367
+
368
+ // Should still concatenate the successful scenes
369
+ expect(ffmpeg.concatenateVideos).toHaveBeenCalledWith(
370
+ expect.arrayContaining([
371
+ expect.stringContaining('scene-0-'),
372
+ expect.stringContaining('scene-2-'),
373
+ ]),
374
+ expect.any(String),
375
+ expect.any(Object)
376
+ );
377
+ });
378
+
379
+ it('should throw error when all scenes fail', async () => {
380
+ mockProvider.generateVideo.mockRejectedValue(
381
+ new Error('Generation failed')
382
+ );
383
+
384
+ const options = {
385
+ apiKey: 'test-api-key',
386
+ scenes: ['Scene 1', 'Scene 2'],
387
+ };
388
+
389
+ await expect(generateStoryboardVideo(options)).rejects.toThrow(
390
+ 'All scenes failed to generate'
391
+ );
392
+ });
393
+
394
+ it('should throw error when no scenes provided', async () => {
395
+ const options = {
396
+ apiKey: 'test-api-key',
397
+ scenes: [],
398
+ };
399
+
400
+ await expect(generateStoryboardVideo(options)).rejects.toThrow(
401
+ 'At least one scene is required'
402
+ );
403
+ });
404
+
405
+ it('should throw error when FFmpeg is not installed', async () => {
406
+ (ffmpeg.checkFfmpegInstalled as any).mockResolvedValue(false);
407
+
408
+ const options = {
409
+ apiKey: 'test-api-key',
410
+ scenes: ['Scene 1'],
411
+ };
412
+
413
+ await expect(generateStoryboardVideo(options)).rejects.toThrow(
414
+ 'FFmpeg is not installed'
415
+ );
416
+ });
417
+
418
+ it('should clean up temporary files even when stitching fails', async () => {
419
+ (ffmpeg.concatenateVideos as any).mockRejectedValue(
420
+ new Error('Stitching failed')
421
+ );
422
+
423
+ const options = {
424
+ apiKey: 'test-api-key',
425
+ scenes: ['Scene 1', 'Scene 2'],
426
+ };
427
+
428
+ await expect(generateStoryboardVideo(options)).rejects.toThrow(
429
+ 'Stitching failed'
430
+ );
431
+
432
+ // Verify cleanup still happened
433
+ expect(fs.unlink).toHaveBeenCalledTimes(2);
434
+ });
435
+ });
436
+
437
+ describe('Progress reporting and timing', () => {
438
+ it('should track timing for each scene', async () => {
439
+ const options = {
440
+ apiKey: 'test-api-key',
441
+ scenes: ['Scene 1', 'Scene 2'],
442
+ };
443
+
444
+ const result = await generateStoryboardVideo(options);
445
+
446
+ expect(result.sceneTimes).toHaveLength(2);
447
+ expect(result.sceneTimes[0]).toMatchObject({
448
+ scene: 1,
449
+ time: expect.any(Number),
450
+ });
451
+ expect(result.sceneTimes[1]).toMatchObject({
452
+ scene: 2,
453
+ time: expect.any(Number),
454
+ });
455
+ });
456
+
457
+ it('should track total generation time', async () => {
458
+ const options = {
459
+ apiKey: 'test-api-key',
460
+ scenes: ['Scene 1'],
461
+ };
462
+
463
+ const result = await generateStoryboardVideo(options);
464
+
465
+ expect(result.totalTime).toBeGreaterThanOrEqual(0);
466
+ expect(result.totalTime).toBeTypeOf('number');
467
+ });
468
+ });
469
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "resolveJsonModule": true,
17
+ "noUncheckedIndexedAccess": true,
18
+ "noImplicitReturns": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true
22
+ },
23
+ "include": ["src/**/*"],
24
+ "exclude": ["node_modules", "dist", "tests"]
25
+ }