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.
- package/.ralph-events.json +151 -0
- package/.ralph-last-branch +1 -0
- package/.ralph-monitor-state.json +7 -0
- package/.ralph-monitor.pid +1 -0
- package/.ralph-timing.json +26 -0
- package/README.md +708 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/platforms/android.d.ts +94 -0
- package/dist/platforms/android.d.ts.map +1 -0
- package/dist/platforms/android.js +123 -0
- package/dist/platforms/android.js.map +1 -0
- package/dist/platforms/ios.d.ts +51 -0
- package/dist/platforms/ios.d.ts.map +1 -0
- package/dist/platforms/ios.js +149 -0
- package/dist/platforms/ios.js.map +1 -0
- package/dist/platforms/macos.d.ts +33 -0
- package/dist/platforms/macos.d.ts.map +1 -0
- package/dist/platforms/macos.js +50 -0
- package/dist/platforms/macos.js.map +1 -0
- package/dist/platforms/watchos.d.ts +36 -0
- package/dist/platforms/watchos.d.ts.map +1 -0
- package/dist/platforms/watchos.js +113 -0
- package/dist/platforms/watchos.js.map +1 -0
- package/dist/platforms/web.d.ts +64 -0
- package/dist/platforms/web.d.ts.map +1 -0
- package/dist/platforms/web.js +96 -0
- package/dist/platforms/web.js.map +1 -0
- package/dist/providers/gemini.d.ts +41 -0
- package/dist/providers/gemini.d.ts.map +1 -0
- package/dist/providers/gemini.js +177 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/tools/analyze/compare.d.ts +12 -0
- package/dist/tools/analyze/compare.d.ts.map +1 -0
- package/dist/tools/analyze/compare.js +83 -0
- package/dist/tools/analyze/compare.js.map +1 -0
- package/dist/tools/analyze/mockup.d.ts +12 -0
- package/dist/tools/analyze/mockup.d.ts.map +1 -0
- package/dist/tools/analyze/mockup.js +88 -0
- package/dist/tools/analyze/mockup.js.map +1 -0
- package/dist/tools/analyze/screenshot.d.ts +12 -0
- package/dist/tools/analyze/screenshot.d.ts.map +1 -0
- package/dist/tools/analyze/screenshot.js +61 -0
- package/dist/tools/analyze/screenshot.js.map +1 -0
- package/dist/tools/app-assets/app-icon.d.ts +9 -0
- package/dist/tools/app-assets/app-icon.d.ts.map +1 -0
- package/dist/tools/app-assets/app-icon.js +133 -0
- package/dist/tools/app-assets/app-icon.js.map +1 -0
- package/dist/tools/app-assets/device-mockup.d.ts +9 -0
- package/dist/tools/app-assets/device-mockup.d.ts.map +1 -0
- package/dist/tools/app-assets/device-mockup.js +139 -0
- package/dist/tools/app-assets/device-mockup.js.map +1 -0
- package/dist/tools/app-assets/launch-images.d.ts +3 -0
- package/dist/tools/app-assets/launch-images.d.ts.map +1 -0
- package/dist/tools/app-assets/launch-images.js +171 -0
- package/dist/tools/app-assets/launch-images.js.map +1 -0
- package/dist/tools/app-assets/resize-devices.d.ts +14 -0
- package/dist/tools/app-assets/resize-devices.d.ts.map +1 -0
- package/dist/tools/app-assets/resize-devices.js +296 -0
- package/dist/tools/app-assets/resize-devices.js.map +1 -0
- package/dist/tools/app-assets/screenshots.d.ts +14 -0
- package/dist/tools/app-assets/screenshots.d.ts.map +1 -0
- package/dist/tools/app-assets/screenshots.js +186 -0
- package/dist/tools/app-assets/screenshots.js.map +1 -0
- package/dist/tools/core/edit-image.d.ts +12 -0
- package/dist/tools/core/edit-image.d.ts.map +1 -0
- package/dist/tools/core/edit-image.js +102 -0
- package/dist/tools/core/edit-image.js.map +1 -0
- package/dist/tools/core/generate-image.d.ts +12 -0
- package/dist/tools/core/generate-image.d.ts.map +1 -0
- package/dist/tools/core/generate-image.js +96 -0
- package/dist/tools/core/generate-image.js.map +1 -0
- package/dist/tools/core/restore-image.d.ts +12 -0
- package/dist/tools/core/restore-image.d.ts.map +1 -0
- package/dist/tools/core/restore-image.js +104 -0
- package/dist/tools/core/restore-image.js.map +1 -0
- package/dist/tools/design/mockup-to-code.d.ts +3 -0
- package/dist/tools/design/mockup-to-code.d.ts.map +1 -0
- package/dist/tools/design/mockup-to-code.js +311 -0
- package/dist/tools/design/mockup-to-code.js.map +1 -0
- package/dist/tools/design/sketch-to-code.d.ts +3 -0
- package/dist/tools/design/sketch-to-code.d.ts.map +1 -0
- package/dist/tools/design/sketch-to-code.js +325 -0
- package/dist/tools/design/sketch-to-code.js.map +1 -0
- package/dist/tools/docs/architecture-diagram.d.ts +12 -0
- package/dist/tools/docs/architecture-diagram.d.ts.map +1 -0
- package/dist/tools/docs/architecture-diagram.js +179 -0
- package/dist/tools/docs/architecture-diagram.js.map +1 -0
- package/dist/tools/docs/readme-banner.d.ts +6 -0
- package/dist/tools/docs/readme-banner.d.ts.map +1 -0
- package/dist/tools/docs/readme-banner.js +108 -0
- package/dist/tools/docs/readme-banner.js.map +1 -0
- package/dist/tools/docs/sequence-diagram.d.ts +12 -0
- package/dist/tools/docs/sequence-diagram.d.ts.map +1 -0
- package/dist/tools/docs/sequence-diagram.js +161 -0
- package/dist/tools/docs/sequence-diagram.js.map +1 -0
- package/dist/tools/docs/social-preview.d.ts +11 -0
- package/dist/tools/docs/social-preview.d.ts.map +1 -0
- package/dist/tools/docs/social-preview.js +111 -0
- package/dist/tools/docs/social-preview.js.map +1 -0
- package/dist/tools/video/extend-video.d.ts +14 -0
- package/dist/tools/video/extend-video.d.ts.map +1 -0
- package/dist/tools/video/extend-video.js +39 -0
- package/dist/tools/video/extend-video.js.map +1 -0
- package/dist/tools/video/generate-video.d.ts +14 -0
- package/dist/tools/video/generate-video.d.ts.map +1 -0
- package/dist/tools/video/generate-video.js +39 -0
- package/dist/tools/video/generate-video.js.map +1 -0
- package/dist/tools/video/image-to-video.d.ts +15 -0
- package/dist/tools/video/image-to-video.d.ts.map +1 -0
- package/dist/tools/video/image-to-video.js +42 -0
- package/dist/tools/video/image-to-video.js.map +1 -0
- package/dist/tools/video/storyboard-video.d.ts +91 -0
- package/dist/tools/video/storyboard-video.d.ts.map +1 -0
- package/dist/tools/video/storyboard-video.js +230 -0
- package/dist/tools/video/storyboard-video.js.map +1 -0
- package/dist/utils/ffmpeg.d.ts +30 -0
- package/dist/utils/ffmpeg.d.ts.map +1 -0
- package/dist/utils/ffmpeg.js +205 -0
- package/dist/utils/ffmpeg.js.map +1 -0
- package/dist/utils/file-handler.d.ts +7 -0
- package/dist/utils/file-handler.d.ts.map +1 -0
- package/dist/utils/file-handler.js +10 -0
- package/dist/utils/file-handler.js.map +1 -0
- package/dist/utils/image-processing.d.ts +7 -0
- package/dist/utils/image-processing.d.ts.map +1 -0
- package/dist/utils/image-processing.js +10 -0
- package/dist/utils/image-processing.js.map +1 -0
- package/docs/PLUGIN-VERIFICATION.md +182 -0
- package/logs/notifications.jsonl +46 -0
- package/package.json +61 -0
- package/prd.json +216 -0
- package/progress.txt +145 -0
- package/ralph-report.html +297 -0
- package/src/index.ts +23 -0
- package/src/platforms/android/.gitkeep +0 -0
- package/src/platforms/ios/.gitkeep +0 -0
- package/src/platforms/web/.gitkeep +0 -0
- package/src/providers/.gitkeep +0 -0
- package/src/providers/gemini.ts +288 -0
- package/src/tools/core/.gitkeep +0 -0
- package/src/tools/platform/.gitkeep +0 -0
- package/src/tools/video/extend-video.ts +71 -0
- package/src/tools/video/generate-video.ts +70 -0
- package/src/tools/video/image-to-video.ts +76 -0
- package/src/tools/video/storyboard-video.ts +325 -0
- package/src/utils/.gitkeep +0 -0
- package/src/utils/ffmpeg.ts +266 -0
- package/src/utils/file-handler.ts +10 -0
- package/src/utils/image-processing.ts +10 -0
- package/templates/.gitkeep +0 -0
- package/test-analyze-screenshot.ts +50 -0
- package/test-app-icons.ts +55 -0
- package/test-cat-sunset.ts +30 -0
- package/test-full-plugin.ts +88 -0
- package/test-icon-gen.ts +30 -0
- package/test-output/test-edit.png +0 -0
- package/test-output/test-generate.png +0 -0
- package/test-output/test-video.mp4 +0 -0
- package/test-plugin-load.js +45 -0
- package/test-princess-emma-continue.ts +35 -0
- package/test-princess-emma-full.ts +38 -0
- package/test-princess-emma-short.ts +32 -0
- package/test-princess-emma-with-reference.ts +34 -0
- package/test-princess-emma.ts +38 -0
- package/test-product-ad.ts +66 -0
- package/test-ralph-droid.ts +30 -0
- package/test-social-preview.ts +61 -0
- package/test-veo31-live.ts +187 -0
- package/test-video-gen.ts +40 -0
- package/test-video-veo.ts +73 -0
- package/test-zurich-video.ts +64 -0
- package/tests/.gitkeep +0 -0
- package/tests/providers/gemini.test.ts +388 -0
- package/tests/utils/ffmpeg.test.ts +328 -0
- package/tests/video/storyboard.test.ts +469 -0
- 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
|
+
}
|