@vibeframe/cli 0.55.0 → 0.55.2
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/dist/index.js +63450 -233
- package/package.json +11 -6
- package/dist/agent/adapters/claude.d.ts +0 -15
- package/dist/agent/adapters/claude.d.ts.map +0 -1
- package/dist/agent/adapters/claude.js +0 -119
- package/dist/agent/adapters/claude.js.map +0 -1
- package/dist/agent/adapters/gemini.d.ts +0 -15
- package/dist/agent/adapters/gemini.d.ts.map +0 -1
- package/dist/agent/adapters/gemini.js +0 -132
- package/dist/agent/adapters/gemini.js.map +0 -1
- package/dist/agent/adapters/index.d.ts +0 -28
- package/dist/agent/adapters/index.d.ts.map +0 -1
- package/dist/agent/adapters/index.js +0 -43
- package/dist/agent/adapters/index.js.map +0 -1
- package/dist/agent/adapters/ollama.d.ts +0 -20
- package/dist/agent/adapters/ollama.d.ts.map +0 -1
- package/dist/agent/adapters/ollama.js +0 -186
- package/dist/agent/adapters/ollama.js.map +0 -1
- package/dist/agent/adapters/openai.d.ts +0 -15
- package/dist/agent/adapters/openai.d.ts.map +0 -1
- package/dist/agent/adapters/openai.js +0 -92
- package/dist/agent/adapters/openai.js.map +0 -1
- package/dist/agent/adapters/openrouter.d.ts +0 -16
- package/dist/agent/adapters/openrouter.d.ts.map +0 -1
- package/dist/agent/adapters/openrouter.js +0 -100
- package/dist/agent/adapters/openrouter.js.map +0 -1
- package/dist/agent/adapters/openrouter.test.d.ts +0 -5
- package/dist/agent/adapters/openrouter.test.d.ts.map +0 -1
- package/dist/agent/adapters/openrouter.test.js +0 -73
- package/dist/agent/adapters/openrouter.test.js.map +0 -1
- package/dist/agent/adapters/xai.d.ts +0 -15
- package/dist/agent/adapters/xai.d.ts.map +0 -1
- package/dist/agent/adapters/xai.js +0 -95
- package/dist/agent/adapters/xai.js.map +0 -1
- package/dist/agent/index.d.ts +0 -69
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/index.js +0 -180
- package/dist/agent/index.js.map +0 -1
- package/dist/agent/memory/index.d.ts +0 -70
- package/dist/agent/memory/index.d.ts.map +0 -1
- package/dist/agent/memory/index.js +0 -132
- package/dist/agent/memory/index.js.map +0 -1
- package/dist/agent/prompts/system.d.ts +0 -6
- package/dist/agent/prompts/system.d.ts.map +0 -1
- package/dist/agent/prompts/system.js +0 -103
- package/dist/agent/prompts/system.js.map +0 -1
- package/dist/agent/tools/ai-editing.d.ts +0 -15
- package/dist/agent/tools/ai-editing.d.ts.map +0 -1
- package/dist/agent/tools/ai-editing.js +0 -763
- package/dist/agent/tools/ai-editing.js.map +0 -1
- package/dist/agent/tools/ai-generation.d.ts +0 -13
- package/dist/agent/tools/ai-generation.d.ts.map +0 -1
- package/dist/agent/tools/ai-generation.js +0 -973
- package/dist/agent/tools/ai-generation.js.map +0 -1
- package/dist/agent/tools/ai-pipeline.d.ts +0 -14
- package/dist/agent/tools/ai-pipeline.d.ts.map +0 -1
- package/dist/agent/tools/ai-pipeline.js +0 -961
- package/dist/agent/tools/ai-pipeline.js.map +0 -1
- package/dist/agent/tools/ai.d.ts +0 -13
- package/dist/agent/tools/ai.d.ts.map +0 -1
- package/dist/agent/tools/ai.js +0 -19
- package/dist/agent/tools/ai.js.map +0 -1
- package/dist/agent/tools/batch.d.ts +0 -6
- package/dist/agent/tools/batch.d.ts.map +0 -1
- package/dist/agent/tools/batch.js +0 -383
- package/dist/agent/tools/batch.js.map +0 -1
- package/dist/agent/tools/e2e.test.d.ts +0 -26
- package/dist/agent/tools/e2e.test.d.ts.map +0 -1
- package/dist/agent/tools/e2e.test.js +0 -397
- package/dist/agent/tools/e2e.test.js.map +0 -1
- package/dist/agent/tools/export.d.ts +0 -6
- package/dist/agent/tools/export.d.ts.map +0 -1
- package/dist/agent/tools/export.js +0 -171
- package/dist/agent/tools/export.js.map +0 -1
- package/dist/agent/tools/filesystem.d.ts +0 -6
- package/dist/agent/tools/filesystem.d.ts.map +0 -1
- package/dist/agent/tools/filesystem.js +0 -212
- package/dist/agent/tools/filesystem.js.map +0 -1
- package/dist/agent/tools/index.d.ts +0 -66
- package/dist/agent/tools/index.d.ts.map +0 -1
- package/dist/agent/tools/index.js +0 -123
- package/dist/agent/tools/index.js.map +0 -1
- package/dist/agent/tools/integration.test.d.ts +0 -11
- package/dist/agent/tools/integration.test.d.ts.map +0 -1
- package/dist/agent/tools/integration.test.js +0 -659
- package/dist/agent/tools/integration.test.js.map +0 -1
- package/dist/agent/tools/media.d.ts +0 -6
- package/dist/agent/tools/media.d.ts.map +0 -1
- package/dist/agent/tools/media.js +0 -616
- package/dist/agent/tools/media.js.map +0 -1
- package/dist/agent/tools/project.d.ts +0 -6
- package/dist/agent/tools/project.d.ts.map +0 -1
- package/dist/agent/tools/project.js +0 -284
- package/dist/agent/tools/project.js.map +0 -1
- package/dist/agent/tools/scene.d.ts +0 -17
- package/dist/agent/tools/scene.d.ts.map +0 -1
- package/dist/agent/tools/scene.js +0 -263
- package/dist/agent/tools/scene.js.map +0 -1
- package/dist/agent/tools/scene.test.d.ts +0 -2
- package/dist/agent/tools/scene.test.d.ts.map +0 -1
- package/dist/agent/tools/scene.test.js +0 -139
- package/dist/agent/tools/scene.test.js.map +0 -1
- package/dist/agent/tools/timeline.d.ts +0 -6
- package/dist/agent/tools/timeline.d.ts.map +0 -1
- package/dist/agent/tools/timeline.js +0 -875
- package/dist/agent/tools/timeline.js.map +0 -1
- package/dist/agent/types.d.ts +0 -59
- package/dist/agent/types.d.ts.map +0 -1
- package/dist/agent/types.js +0 -5
- package/dist/agent/types.js.map +0 -1
- package/dist/commands/_shared/scene-audio-mux.d.ts +0 -75
- package/dist/commands/_shared/scene-audio-mux.d.ts.map +0 -1
- package/dist/commands/_shared/scene-audio-mux.js +0 -159
- package/dist/commands/_shared/scene-audio-mux.js.map +0 -1
- package/dist/commands/_shared/scene-audio-mux.test.d.ts +0 -7
- package/dist/commands/_shared/scene-audio-mux.test.d.ts.map +0 -1
- package/dist/commands/_shared/scene-audio-mux.test.js +0 -94
- package/dist/commands/_shared/scene-audio-mux.test.js.map +0 -1
- package/dist/commands/_shared/scene-audio-scan.d.ts +0 -94
- package/dist/commands/_shared/scene-audio-scan.d.ts.map +0 -1
- package/dist/commands/_shared/scene-audio-scan.js +0 -144
- package/dist/commands/_shared/scene-audio-scan.js.map +0 -1
- package/dist/commands/_shared/scene-audio-scan.test.d.ts +0 -6
- package/dist/commands/_shared/scene-audio-scan.test.d.ts.map +0 -1
- package/dist/commands/_shared/scene-audio-scan.test.js +0 -190
- package/dist/commands/_shared/scene-audio-scan.test.js.map +0 -1
- package/dist/commands/_shared/scene-html-emit.d.ts +0 -120
- package/dist/commands/_shared/scene-html-emit.d.ts.map +0 -1
- package/dist/commands/_shared/scene-html-emit.js +0 -422
- package/dist/commands/_shared/scene-html-emit.js.map +0 -1
- package/dist/commands/_shared/scene-html-emit.test.d.ts +0 -2
- package/dist/commands/_shared/scene-html-emit.test.d.ts.map +0 -1
- package/dist/commands/_shared/scene-html-emit.test.js +0 -396
- package/dist/commands/_shared/scene-html-emit.test.js.map +0 -1
- package/dist/commands/_shared/scene-lint.d.ts +0 -110
- package/dist/commands/_shared/scene-lint.d.ts.map +0 -1
- package/dist/commands/_shared/scene-lint.js +0 -207
- package/dist/commands/_shared/scene-lint.js.map +0 -1
- package/dist/commands/_shared/scene-lint.test.d.ts +0 -2
- package/dist/commands/_shared/scene-lint.test.d.ts.map +0 -1
- package/dist/commands/_shared/scene-lint.test.js +0 -208
- package/dist/commands/_shared/scene-lint.test.js.map +0 -1
- package/dist/commands/_shared/scene-project.d.ts +0 -101
- package/dist/commands/_shared/scene-project.d.ts.map +0 -1
- package/dist/commands/_shared/scene-project.js +0 -281
- package/dist/commands/_shared/scene-project.js.map +0 -1
- package/dist/commands/_shared/scene-project.test.d.ts +0 -2
- package/dist/commands/_shared/scene-project.test.d.ts.map +0 -1
- package/dist/commands/_shared/scene-project.test.js +0 -203
- package/dist/commands/_shared/scene-project.test.js.map +0 -1
- package/dist/commands/_shared/scene-render.d.ts +0 -85
- package/dist/commands/_shared/scene-render.d.ts.map +0 -1
- package/dist/commands/_shared/scene-render.js +0 -178
- package/dist/commands/_shared/scene-render.js.map +0 -1
- package/dist/commands/_shared/scene-render.test.d.ts +0 -2
- package/dist/commands/_shared/scene-render.test.d.ts.map +0 -1
- package/dist/commands/_shared/scene-render.test.js +0 -145
- package/dist/commands/_shared/scene-render.test.js.map +0 -1
- package/dist/commands/_shared/segments-to-scenes.d.ts +0 -85
- package/dist/commands/_shared/segments-to-scenes.d.ts.map +0 -1
- package/dist/commands/_shared/segments-to-scenes.js +0 -202
- package/dist/commands/_shared/segments-to-scenes.js.map +0 -1
- package/dist/commands/_shared/segments-to-scenes.test.d.ts +0 -2
- package/dist/commands/_shared/segments-to-scenes.test.d.ts.map +0 -1
- package/dist/commands/_shared/segments-to-scenes.test.js +0 -219
- package/dist/commands/_shared/segments-to-scenes.test.js.map +0 -1
- package/dist/commands/_shared/tts-resolve.d.ts +0 -75
- package/dist/commands/_shared/tts-resolve.d.ts.map +0 -1
- package/dist/commands/_shared/tts-resolve.js +0 -89
- package/dist/commands/_shared/tts-resolve.js.map +0 -1
- package/dist/commands/_shared/tts-resolve.test.d.ts +0 -6
- package/dist/commands/_shared/tts-resolve.test.d.ts.map +0 -1
- package/dist/commands/_shared/tts-resolve.test.js +0 -147
- package/dist/commands/_shared/tts-resolve.test.js.map +0 -1
- package/dist/commands/agent.d.ts +0 -21
- package/dist/commands/agent.d.ts.map +0 -1
- package/dist/commands/agent.js +0 -285
- package/dist/commands/agent.js.map +0 -1
- package/dist/commands/ai-analyze.d.ts +0 -106
- package/dist/commands/ai-analyze.d.ts.map +0 -1
- package/dist/commands/ai-analyze.js +0 -320
- package/dist/commands/ai-analyze.js.map +0 -1
- package/dist/commands/ai-animated-caption.d.ts +0 -64
- package/dist/commands/ai-animated-caption.d.ts.map +0 -1
- package/dist/commands/ai-animated-caption.js +0 -272
- package/dist/commands/ai-animated-caption.js.map +0 -1
- package/dist/commands/ai-audio.d.ts +0 -98
- package/dist/commands/ai-audio.d.ts.map +0 -1
- package/dist/commands/ai-audio.js +0 -1059
- package/dist/commands/ai-audio.js.map +0 -1
- package/dist/commands/ai-broll.d.ts +0 -15
- package/dist/commands/ai-broll.d.ts.map +0 -1
- package/dist/commands/ai-broll.js +0 -421
- package/dist/commands/ai-broll.js.map +0 -1
- package/dist/commands/ai-edit-cli.d.ts +0 -14
- package/dist/commands/ai-edit-cli.d.ts.map +0 -1
- package/dist/commands/ai-edit-cli.js +0 -585
- package/dist/commands/ai-edit-cli.js.map +0 -1
- package/dist/commands/ai-edit.d.ts +0 -398
- package/dist/commands/ai-edit.d.ts.map +0 -1
- package/dist/commands/ai-edit.js +0 -1060
- package/dist/commands/ai-edit.js.map +0 -1
- package/dist/commands/ai-fill-gaps.d.ts +0 -14
- package/dist/commands/ai-fill-gaps.d.ts.map +0 -1
- package/dist/commands/ai-fill-gaps.js +0 -447
- package/dist/commands/ai-fill-gaps.js.map +0 -1
- package/dist/commands/ai-helpers.d.ts +0 -20
- package/dist/commands/ai-helpers.d.ts.map +0 -1
- package/dist/commands/ai-helpers.js +0 -59
- package/dist/commands/ai-helpers.js.map +0 -1
- package/dist/commands/ai-highlights.d.ts +0 -127
- package/dist/commands/ai-highlights.d.ts.map +0 -1
- package/dist/commands/ai-highlights.js +0 -1053
- package/dist/commands/ai-highlights.js.map +0 -1
- package/dist/commands/ai-image.d.ts +0 -76
- package/dist/commands/ai-image.d.ts.map +0 -1
- package/dist/commands/ai-image.js +0 -858
- package/dist/commands/ai-image.js.map +0 -1
- package/dist/commands/ai-motion.d.ts +0 -50
- package/dist/commands/ai-motion.d.ts.map +0 -1
- package/dist/commands/ai-motion.js +0 -297
- package/dist/commands/ai-motion.js.map +0 -1
- package/dist/commands/ai-narrate.d.ts +0 -66
- package/dist/commands/ai-narrate.d.ts.map +0 -1
- package/dist/commands/ai-narrate.js +0 -332
- package/dist/commands/ai-narrate.js.map +0 -1
- package/dist/commands/ai-review.d.ts +0 -57
- package/dist/commands/ai-review.d.ts.map +0 -1
- package/dist/commands/ai-review.js +0 -270
- package/dist/commands/ai-review.js.map +0 -1
- package/dist/commands/ai-script-pipeline-cli.d.ts +0 -9
- package/dist/commands/ai-script-pipeline-cli.d.ts.map +0 -1
- package/dist/commands/ai-script-pipeline-cli.js +0 -502
- package/dist/commands/ai-script-pipeline-cli.js.map +0 -1
- package/dist/commands/ai-script-pipeline.d.ts +0 -305
- package/dist/commands/ai-script-pipeline.d.ts.map +0 -1
- package/dist/commands/ai-script-pipeline.js +0 -1546
- package/dist/commands/ai-script-pipeline.js.map +0 -1
- package/dist/commands/ai-suggest-edit.d.ts +0 -14
- package/dist/commands/ai-suggest-edit.d.ts.map +0 -1
- package/dist/commands/ai-suggest-edit.js +0 -215
- package/dist/commands/ai-suggest-edit.js.map +0 -1
- package/dist/commands/ai-video-fx.d.ts +0 -14
- package/dist/commands/ai-video-fx.d.ts.map +0 -1
- package/dist/commands/ai-video-fx.js +0 -396
- package/dist/commands/ai-video-fx.js.map +0 -1
- package/dist/commands/ai-video.d.ts +0 -91
- package/dist/commands/ai-video.d.ts.map +0 -1
- package/dist/commands/ai-video.js +0 -1055
- package/dist/commands/ai-video.js.map +0 -1
- package/dist/commands/ai-viral.d.ts +0 -15
- package/dist/commands/ai-viral.d.ts.map +0 -1
- package/dist/commands/ai-viral.js +0 -507
- package/dist/commands/ai-viral.js.map +0 -1
- package/dist/commands/ai-visual-fx.d.ts +0 -14
- package/dist/commands/ai-visual-fx.d.ts.map +0 -1
- package/dist/commands/ai-visual-fx.js +0 -521
- package/dist/commands/ai-visual-fx.js.map +0 -1
- package/dist/commands/ai.d.ts +0 -38
- package/dist/commands/ai.d.ts.map +0 -1
- package/dist/commands/ai.js +0 -225
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/ai.test.d.ts +0 -2
- package/dist/commands/ai.test.d.ts.map +0 -1
- package/dist/commands/ai.test.js +0 -557
- package/dist/commands/ai.test.js.map +0 -1
- package/dist/commands/analyze.d.ts +0 -16
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js +0 -247
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/audio.d.ts +0 -18
- package/dist/commands/audio.d.ts.map +0 -1
- package/dist/commands/audio.js +0 -539
- package/dist/commands/audio.js.map +0 -1
- package/dist/commands/batch.d.ts +0 -3
- package/dist/commands/batch.d.ts.map +0 -1
- package/dist/commands/batch.js +0 -419
- package/dist/commands/batch.js.map +0 -1
- package/dist/commands/batch.test.d.ts +0 -2
- package/dist/commands/batch.test.d.ts.map +0 -1
- package/dist/commands/batch.test.js +0 -203
- package/dist/commands/batch.test.js.map +0 -1
- package/dist/commands/context.d.ts +0 -7
- package/dist/commands/context.d.ts.map +0 -1
- package/dist/commands/context.js +0 -66
- package/dist/commands/context.js.map +0 -1
- package/dist/commands/demo.d.ts +0 -7
- package/dist/commands/demo.d.ts.map +0 -1
- package/dist/commands/demo.js +0 -192
- package/dist/commands/demo.js.map +0 -1
- package/dist/commands/detect.d.ts +0 -56
- package/dist/commands/detect.d.ts.map +0 -1
- package/dist/commands/detect.js +0 -464
- package/dist/commands/detect.js.map +0 -1
- package/dist/commands/doctor.d.ts +0 -6
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js +0 -322
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/edit-cmd.d.ts +0 -112
- package/dist/commands/edit-cmd.d.ts.map +0 -1
- package/dist/commands/edit-cmd.js +0 -1041
- package/dist/commands/edit-cmd.js.map +0 -1
- package/dist/commands/export.d.ts +0 -61
- package/dist/commands/export.d.ts.map +0 -1
- package/dist/commands/export.js +0 -980
- package/dist/commands/export.js.map +0 -1
- package/dist/commands/export.test.d.ts +0 -2
- package/dist/commands/export.test.d.ts.map +0 -1
- package/dist/commands/export.test.js +0 -107
- package/dist/commands/export.test.js.map +0 -1
- package/dist/commands/generate.d.ts +0 -84
- package/dist/commands/generate.d.ts.map +0 -1
- package/dist/commands/generate.js +0 -2030
- package/dist/commands/generate.js.map +0 -1
- package/dist/commands/media.d.ts +0 -3
- package/dist/commands/media.d.ts.map +0 -1
- package/dist/commands/media.js +0 -163
- package/dist/commands/media.js.map +0 -1
- package/dist/commands/output.d.ts +0 -51
- package/dist/commands/output.d.ts.map +0 -1
- package/dist/commands/output.js +0 -219
- package/dist/commands/output.js.map +0 -1
- package/dist/commands/output.test.d.ts +0 -2
- package/dist/commands/output.test.d.ts.map +0 -1
- package/dist/commands/output.test.js +0 -151
- package/dist/commands/output.test.js.map +0 -1
- package/dist/commands/pipeline.d.ts +0 -19
- package/dist/commands/pipeline.d.ts.map +0 -1
- package/dist/commands/pipeline.js +0 -345
- package/dist/commands/pipeline.js.map +0 -1
- package/dist/commands/project.d.ts +0 -3
- package/dist/commands/project.d.ts.map +0 -1
- package/dist/commands/project.js +0 -181
- package/dist/commands/project.js.map +0 -1
- package/dist/commands/project.test.d.ts +0 -2
- package/dist/commands/project.test.d.ts.map +0 -1
- package/dist/commands/project.test.js +0 -105
- package/dist/commands/project.test.js.map +0 -1
- package/dist/commands/run.d.ts +0 -15
- package/dist/commands/run.d.ts.map +0 -1
- package/dist/commands/run.js +0 -204
- package/dist/commands/run.js.map +0 -1
- package/dist/commands/sanitize.d.ts +0 -21
- package/dist/commands/sanitize.d.ts.map +0 -1
- package/dist/commands/sanitize.js +0 -56
- package/dist/commands/sanitize.js.map +0 -1
- package/dist/commands/scene.d.ts +0 -86
- package/dist/commands/scene.d.ts.map +0 -1
- package/dist/commands/scene.js +0 -728
- package/dist/commands/scene.js.map +0 -1
- package/dist/commands/scene.test.d.ts +0 -2
- package/dist/commands/scene.test.d.ts.map +0 -1
- package/dist/commands/scene.test.js +0 -219
- package/dist/commands/scene.test.js.map +0 -1
- package/dist/commands/schema.d.ts +0 -13
- package/dist/commands/schema.d.ts.map +0 -1
- package/dist/commands/schema.js +0 -218
- package/dist/commands/schema.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -6
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -461
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/timeline.d.ts +0 -3
- package/dist/commands/timeline.d.ts.map +0 -1
- package/dist/commands/timeline.js +0 -609
- package/dist/commands/timeline.js.map +0 -1
- package/dist/commands/timeline.test.d.ts +0 -2
- package/dist/commands/timeline.test.d.ts.map +0 -1
- package/dist/commands/timeline.test.js +0 -334
- package/dist/commands/timeline.test.js.map +0 -1
- package/dist/commands/validate.d.ts +0 -34
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/validate.js +0 -70
- package/dist/commands/validate.js.map +0 -1
- package/dist/config/config.test.d.ts +0 -2
- package/dist/config/config.test.d.ts.map +0 -1
- package/dist/config/config.test.js +0 -164
- package/dist/config/config.test.js.map +0 -1
- package/dist/config/index.d.ts +0 -35
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js +0 -101
- package/dist/config/index.js.map +0 -1
- package/dist/config/schema.d.ts +0 -52
- package/dist/config/schema.d.ts.map +0 -1
- package/dist/config/schema.js +0 -44
- package/dist/config/schema.js.map +0 -1
- package/dist/engine/index.d.ts +0 -3
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -2
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/project.d.ts +0 -84
- package/dist/engine/project.d.ts.map +0 -1
- package/dist/engine/project.js +0 -355
- package/dist/engine/project.js.map +0 -1
- package/dist/engine/project.test.d.ts +0 -2
- package/dist/engine/project.test.d.ts.map +0 -1
- package/dist/engine/project.test.js +0 -599
- package/dist/engine/project.test.js.map +0 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/pipeline/executor.d.ts +0 -19
- package/dist/pipeline/executor.d.ts.map +0 -1
- package/dist/pipeline/executor.js +0 -397
- package/dist/pipeline/executor.js.map +0 -1
- package/dist/pipeline/index.d.ts +0 -4
- package/dist/pipeline/index.d.ts.map +0 -1
- package/dist/pipeline/index.js +0 -2
- package/dist/pipeline/index.js.map +0 -1
- package/dist/pipeline/renderers/__tests__/html-template.test.d.ts +0 -2
- package/dist/pipeline/renderers/__tests__/html-template.test.d.ts.map +0 -1
- package/dist/pipeline/renderers/__tests__/html-template.test.js +0 -170
- package/dist/pipeline/renderers/__tests__/html-template.test.js.map +0 -1
- package/dist/pipeline/renderers/__tests__/integration.test.d.ts +0 -2
- package/dist/pipeline/renderers/__tests__/integration.test.d.ts.map +0 -1
- package/dist/pipeline/renderers/__tests__/integration.test.js +0 -175
- package/dist/pipeline/renderers/__tests__/integration.test.js.map +0 -1
- package/dist/pipeline/renderers/chrome.d.ts +0 -26
- package/dist/pipeline/renderers/chrome.d.ts.map +0 -1
- package/dist/pipeline/renderers/chrome.js +0 -48
- package/dist/pipeline/renderers/chrome.js.map +0 -1
- package/dist/pipeline/renderers/html-clips.d.ts +0 -20
- package/dist/pipeline/renderers/html-clips.d.ts.map +0 -1
- package/dist/pipeline/renderers/html-clips.js +0 -50
- package/dist/pipeline/renderers/html-clips.js.map +0 -1
- package/dist/pipeline/renderers/html-runtime.d.ts +0 -7
- package/dist/pipeline/renderers/html-runtime.d.ts.map +0 -1
- package/dist/pipeline/renderers/html-runtime.js +0 -80
- package/dist/pipeline/renderers/html-runtime.js.map +0 -1
- package/dist/pipeline/renderers/html-template.d.ts +0 -3
- package/dist/pipeline/renderers/html-template.d.ts.map +0 -1
- package/dist/pipeline/renderers/html-template.js +0 -36
- package/dist/pipeline/renderers/html-template.js.map +0 -1
- package/dist/pipeline/renderers/hyperframes.d.ts +0 -8
- package/dist/pipeline/renderers/hyperframes.d.ts.map +0 -1
- package/dist/pipeline/renderers/hyperframes.js +0 -63
- package/dist/pipeline/renderers/hyperframes.js.map +0 -1
- package/dist/pipeline/renderers/project-builder.d.ts +0 -8
- package/dist/pipeline/renderers/project-builder.d.ts.map +0 -1
- package/dist/pipeline/renderers/project-builder.js +0 -65
- package/dist/pipeline/renderers/project-builder.js.map +0 -1
- package/dist/pipeline/renderers/types.d.ts +0 -41
- package/dist/pipeline/renderers/types.d.ts.map +0 -1
- package/dist/pipeline/renderers/types.js +0 -11
- package/dist/pipeline/renderers/types.js.map +0 -1
- package/dist/pipeline/resolver.d.ts +0 -20
- package/dist/pipeline/resolver.d.ts.map +0 -1
- package/dist/pipeline/resolver.js +0 -74
- package/dist/pipeline/resolver.js.map +0 -1
- package/dist/pipeline/types.d.ts +0 -82
- package/dist/pipeline/types.d.ts.map +0 -1
- package/dist/pipeline/types.js +0 -8
- package/dist/pipeline/types.js.map +0 -1
- package/dist/utils/api-key.d.ts +0 -33
- package/dist/utils/api-key.d.ts.map +0 -1
- package/dist/utils/api-key.js +0 -228
- package/dist/utils/api-key.js.map +0 -1
- package/dist/utils/api-key.test.d.ts +0 -2
- package/dist/utils/api-key.test.d.ts.map +0 -1
- package/dist/utils/api-key.test.js +0 -35
- package/dist/utils/api-key.test.js.map +0 -1
- package/dist/utils/audio.d.ts +0 -23
- package/dist/utils/audio.d.ts.map +0 -1
- package/dist/utils/audio.js +0 -79
- package/dist/utils/audio.js.map +0 -1
- package/dist/utils/exec-safe.d.ts +0 -22
- package/dist/utils/exec-safe.d.ts.map +0 -1
- package/dist/utils/exec-safe.js +0 -62
- package/dist/utils/exec-safe.js.map +0 -1
- package/dist/utils/first-run.d.ts +0 -17
- package/dist/utils/first-run.d.ts.map +0 -1
- package/dist/utils/first-run.js +0 -73
- package/dist/utils/first-run.js.map +0 -1
- package/dist/utils/input-validation.d.ts +0 -27
- package/dist/utils/input-validation.d.ts.map +0 -1
- package/dist/utils/input-validation.js +0 -67
- package/dist/utils/input-validation.js.map +0 -1
- package/dist/utils/provider-resolver.d.ts +0 -19
- package/dist/utils/provider-resolver.d.ts.map +0 -1
- package/dist/utils/provider-resolver.js +0 -75
- package/dist/utils/provider-resolver.js.map +0 -1
- package/dist/utils/remotion.d.ts +0 -231
- package/dist/utils/remotion.d.ts.map +0 -1
- package/dist/utils/remotion.js +0 -879
- package/dist/utils/remotion.js.map +0 -1
- package/dist/utils/subtitle.d.ts +0 -65
- package/dist/utils/subtitle.d.ts.map +0 -1
- package/dist/utils/subtitle.js +0 -135
- package/dist/utils/subtitle.js.map +0 -1
- package/dist/utils/subtitle.test.d.ts +0 -2
- package/dist/utils/subtitle.test.d.ts.map +0 -1
- package/dist/utils/subtitle.test.js +0 -175
- package/dist/utils/subtitle.test.js.map +0 -1
- package/dist/utils/tty.d.ts +0 -45
- package/dist/utils/tty.d.ts.map +0 -1
- package/dist/utils/tty.js +0 -231
- package/dist/utils/tty.js.map +0 -1
|
@@ -1,1546 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module ai-script-pipeline
|
|
3
|
-
*
|
|
4
|
-
* Script-to-video pipeline and scene regeneration execute functions.
|
|
5
|
-
*
|
|
6
|
-
* CLI commands: script-to-video, regenerate-scene
|
|
7
|
-
*
|
|
8
|
-
* Execute functions:
|
|
9
|
-
* executeScriptToVideo - Full pipeline: storyboard -> TTS -> images -> videos -> project
|
|
10
|
-
* executeRegenerateScene - Re-generate specific scene(s) in an existing project
|
|
11
|
-
*
|
|
12
|
-
* Also exports shared helpers: uploadToImgbb, extendVideoToTarget,
|
|
13
|
-
* generateVideoWithRetryKling, generateVideoWithRetryRunway, generateVideoWithRetryVeo, waitForVideoWithRetry
|
|
14
|
-
*
|
|
15
|
-
* @dependencies Claude (storyboard), ElevenLabs (TTS), OpenAI/Gemini (images),
|
|
16
|
-
* Kling/Runway (video), FFmpeg (assembly/extension)
|
|
17
|
-
*/
|
|
18
|
-
import { readFile, writeFile, mkdir, unlink, rename } from "node:fs/promises";
|
|
19
|
-
import { resolve, basename, dirname, extname } from "node:path";
|
|
20
|
-
import { existsSync } from "node:fs";
|
|
21
|
-
import { stringify as yamlStringify, parse as yamlParse } from "yaml";
|
|
22
|
-
import chalk from "chalk";
|
|
23
|
-
import { GeminiProvider, OpenAIProvider, OpenAIImageProvider, ClaudeProvider, ElevenLabsProvider, KlingProvider, RunwayProvider, GrokProvider, } from "@vibeframe/ai-providers";
|
|
24
|
-
import { getApiKey } from "../utils/api-key.js";
|
|
25
|
-
import { getApiKeyFromConfig } from "../config/index.js";
|
|
26
|
-
import { Project } from "../engine/index.js";
|
|
27
|
-
import { getAudioDuration, getVideoDuration, extendVideoNaturally } from "../utils/audio.js";
|
|
28
|
-
import { applyTextOverlays } from "./ai-edit.js";
|
|
29
|
-
import { executeReview } from "./ai-review.js";
|
|
30
|
-
import { execSafe } from "../utils/exec-safe.js";
|
|
31
|
-
import { downloadVideo } from "./ai-helpers.js";
|
|
32
|
-
/** Default retry count for video generation API calls. */
|
|
33
|
-
export const DEFAULT_VIDEO_RETRIES = 2;
|
|
34
|
-
/** Delay between retries in milliseconds. */
|
|
35
|
-
export const RETRY_DELAY_MS = 5000;
|
|
36
|
-
/**
|
|
37
|
-
* Sleep helper
|
|
38
|
-
*/
|
|
39
|
-
export function sleep(ms) {
|
|
40
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Upload image to ImgBB and return the URL
|
|
44
|
-
* Used for Kling v2.5/v2.6 image-to-video which requires URL (not base64)
|
|
45
|
-
*/
|
|
46
|
-
export async function uploadToImgbb(imageBuffer, apiKey) {
|
|
47
|
-
try {
|
|
48
|
-
const base64Image = imageBuffer.toString("base64");
|
|
49
|
-
const formData = new URLSearchParams();
|
|
50
|
-
formData.append("key", apiKey);
|
|
51
|
-
formData.append("image", base64Image);
|
|
52
|
-
const response = await fetch("https://api.imgbb.com/1/upload", {
|
|
53
|
-
method: "POST",
|
|
54
|
-
body: formData,
|
|
55
|
-
});
|
|
56
|
-
if (!response.ok) {
|
|
57
|
-
return { success: false, error: `ImgBB API error (${response.status}): ${response.statusText}` };
|
|
58
|
-
}
|
|
59
|
-
const data = (await response.json());
|
|
60
|
-
if (data.success && data.data?.url) {
|
|
61
|
-
return { success: true, url: data.data.url };
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
return { success: false, error: data.error?.message || "Upload failed" };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
catch (err) {
|
|
68
|
-
return { success: false, error: String(err) };
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Extend a video to target duration using Kling extend API when possible,
|
|
73
|
-
* with fallback to FFmpeg-based extendVideoNaturally.
|
|
74
|
-
*
|
|
75
|
-
* When the extension ratio > 1.4 and a Kling provider + videoId are available,
|
|
76
|
-
* uses the Kling video-extend API for natural continuation instead of freeze frames.
|
|
77
|
-
*/
|
|
78
|
-
export async function extendVideoToTarget(videoPath, targetDuration, outputDir, sceneLabel, options) {
|
|
79
|
-
const actualDuration = await getVideoDuration(videoPath);
|
|
80
|
-
if (actualDuration >= targetDuration - 0.1)
|
|
81
|
-
return;
|
|
82
|
-
const ratio = targetDuration / actualDuration;
|
|
83
|
-
const extendedPath = resolve(outputDir, `${basename(videoPath, ".mp4")}-extended.mp4`);
|
|
84
|
-
// Try Kling extend API for large gaps (ratio > 1.4) where freeze frames look bad
|
|
85
|
-
if (ratio > 1.4 && options?.kling && options?.videoId) {
|
|
86
|
-
try {
|
|
87
|
-
options.onProgress?.(`${sceneLabel}: Extending via Kling API...`);
|
|
88
|
-
const extendResult = await options.kling.extendVideo(options.videoId, {
|
|
89
|
-
duration: "5",
|
|
90
|
-
});
|
|
91
|
-
if (extendResult.status !== "failed" && extendResult.id) {
|
|
92
|
-
const waitResult = await options.kling.waitForExtendCompletion(extendResult.id, (status) => {
|
|
93
|
-
options.onProgress?.(`${sceneLabel}: extend ${status.status}...`);
|
|
94
|
-
}, 600000);
|
|
95
|
-
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
96
|
-
// Download extended video
|
|
97
|
-
const extendedVideoPath = resolve(outputDir, `${basename(videoPath, ".mp4")}-kling-ext.mp4`);
|
|
98
|
-
const buffer = await downloadVideo(waitResult.videoUrl);
|
|
99
|
-
await writeFile(extendedVideoPath, buffer);
|
|
100
|
-
// Concatenate original + extension
|
|
101
|
-
const concatPath = resolve(outputDir, `${basename(videoPath, ".mp4")}-concat.mp4`);
|
|
102
|
-
const listPath = resolve(outputDir, `${basename(videoPath, ".mp4")}-concat.txt`);
|
|
103
|
-
await writeFile(listPath, `file '${videoPath}'\nfile '${extendedVideoPath}'`, "utf-8");
|
|
104
|
-
await execSafe("ffmpeg", ["-y", "-f", "concat", "-safe", "0", "-i", listPath, "-c", "copy", concatPath]);
|
|
105
|
-
// Trim to exact target duration if concatenated video is longer
|
|
106
|
-
const concatDuration = await getVideoDuration(concatPath);
|
|
107
|
-
if (concatDuration > targetDuration + 0.5) {
|
|
108
|
-
await execSafe("ffmpeg", ["-y", "-i", concatPath, "-t", targetDuration.toFixed(2), "-c", "copy", extendedPath]);
|
|
109
|
-
await unlink(concatPath);
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
await rename(concatPath, extendedPath);
|
|
113
|
-
}
|
|
114
|
-
// Cleanup temp files
|
|
115
|
-
await unlink(extendedVideoPath).catch(() => { });
|
|
116
|
-
await unlink(listPath).catch(() => { });
|
|
117
|
-
await unlink(videoPath);
|
|
118
|
-
await rename(extendedPath, videoPath);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
// If Kling extend failed, fall through to FFmpeg fallback
|
|
123
|
-
options.onProgress?.(`${sceneLabel}: Kling extend failed, using FFmpeg fallback...`);
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
options.onProgress?.(`${sceneLabel}: Kling extend error, using FFmpeg fallback...`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
// FFmpeg-based fallback (slowdown + frame interpolation + freeze frame)
|
|
130
|
-
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
131
|
-
await unlink(videoPath);
|
|
132
|
-
await rename(extendedPath, videoPath);
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Log a provider failure to stderr. Centralizes the "what went wrong" output
|
|
136
|
-
* so call sites stop silently dropping provider errors into failedScenes.
|
|
137
|
-
* Safe to call with unknown errors, string messages, or VideoResult shapes.
|
|
138
|
-
*/
|
|
139
|
-
export function logSceneFailure(provider, sceneLabel, err) {
|
|
140
|
-
let msg;
|
|
141
|
-
if (err instanceof Error) {
|
|
142
|
-
msg = err.message;
|
|
143
|
-
}
|
|
144
|
-
else if (typeof err === "string") {
|
|
145
|
-
msg = err;
|
|
146
|
-
}
|
|
147
|
-
else if (err && typeof err === "object" && "error" in err && typeof err.error === "string") {
|
|
148
|
-
msg = err.error;
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
msg = String(err);
|
|
152
|
-
}
|
|
153
|
-
console.error(chalk.dim(`\n [${provider} ${sceneLabel}: ${msg}]`));
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Generate video with retry logic for Grok provider
|
|
157
|
-
*/
|
|
158
|
-
export async function generateVideoWithRetryGrok(grok, segment, options, maxRetries, onProgress) {
|
|
159
|
-
const prompt = segment.visualStyle
|
|
160
|
-
? `${segment.visuals}. Style: ${segment.visualStyle}`
|
|
161
|
-
: segment.visuals;
|
|
162
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
163
|
-
try {
|
|
164
|
-
const result = await grok.generateVideo(prompt, {
|
|
165
|
-
prompt,
|
|
166
|
-
duration: options.duration,
|
|
167
|
-
aspectRatio: options.aspectRatio,
|
|
168
|
-
referenceImage: options.referenceImage,
|
|
169
|
-
});
|
|
170
|
-
if (result.status !== "failed" && result.id) {
|
|
171
|
-
return { requestId: result.id };
|
|
172
|
-
}
|
|
173
|
-
// Provider returned status: "failed" with an error message — don't
|
|
174
|
-
// discard it. Surface via onProgress + stderr so the caller can see WHY.
|
|
175
|
-
const providerErr = result.error || "Grok returned failed status";
|
|
176
|
-
if (attempt < maxRetries) {
|
|
177
|
-
onProgress?.(`⚠ ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
178
|
-
await sleep(RETRY_DELAY_MS);
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
console.error(chalk.dim(`\n [Grok error: ${providerErr}]`));
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
catch (err) {
|
|
185
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
186
|
-
if (attempt < maxRetries) {
|
|
187
|
-
onProgress?.(`⚠ Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
188
|
-
await sleep(RETRY_DELAY_MS);
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
console.error(chalk.dim(`\n [Grok error: ${errMsg}]`));
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Generate video with retry logic for Kling provider
|
|
199
|
-
* Supports image-to-video with URL (v2.5/v2.6 models)
|
|
200
|
-
*/
|
|
201
|
-
export async function generateVideoWithRetryKling(kling, segment, options, maxRetries, onProgress) {
|
|
202
|
-
// Build detailed prompt from storyboard segment
|
|
203
|
-
const prompt = segment.visualStyle
|
|
204
|
-
? `${segment.visuals}. Style: ${segment.visualStyle}`
|
|
205
|
-
: segment.visuals;
|
|
206
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
207
|
-
try {
|
|
208
|
-
const result = await kling.generateVideo(prompt, {
|
|
209
|
-
prompt,
|
|
210
|
-
// Pass reference image (base64 or URL) - KlingProvider handles v1.5 fallback for base64
|
|
211
|
-
referenceImage: options.referenceImage,
|
|
212
|
-
duration: options.duration,
|
|
213
|
-
aspectRatio: options.aspectRatio,
|
|
214
|
-
mode: "std", // Use std mode for faster generation
|
|
215
|
-
});
|
|
216
|
-
if (result.status !== "failed" && result.id) {
|
|
217
|
-
return {
|
|
218
|
-
taskId: result.id,
|
|
219
|
-
type: options.referenceImage ? "image2video" : "text2video",
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
const providerErr = result.error || "Kling returned failed status";
|
|
223
|
-
if (attempt < maxRetries) {
|
|
224
|
-
onProgress?.(`⚠ ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
225
|
-
await sleep(RETRY_DELAY_MS);
|
|
226
|
-
}
|
|
227
|
-
else {
|
|
228
|
-
console.error(chalk.dim(`\n [Kling error: ${providerErr}]`));
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
catch (err) {
|
|
232
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
233
|
-
if (attempt < maxRetries) {
|
|
234
|
-
onProgress?.(`⚠ Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
235
|
-
await sleep(RETRY_DELAY_MS);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
console.error(chalk.dim(`\n [Kling error: ${errMsg}]`));
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Generate video with retry logic for Runway provider
|
|
246
|
-
*/
|
|
247
|
-
export async function generateVideoWithRetryRunway(runway, segment, referenceImage, options, maxRetries, onProgress) {
|
|
248
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
249
|
-
try {
|
|
250
|
-
const result = await runway.generateVideo(segment.visuals, {
|
|
251
|
-
prompt: segment.visuals,
|
|
252
|
-
referenceImage,
|
|
253
|
-
duration: options.duration,
|
|
254
|
-
aspectRatio: options.aspectRatio,
|
|
255
|
-
});
|
|
256
|
-
if (result.status !== "failed" && result.id) {
|
|
257
|
-
return { taskId: result.id };
|
|
258
|
-
}
|
|
259
|
-
const providerErr = result.error || "Runway returned failed status";
|
|
260
|
-
if (attempt < maxRetries) {
|
|
261
|
-
onProgress?.(`⚠ ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
262
|
-
await sleep(RETRY_DELAY_MS);
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
console.error(chalk.dim(`\n [Runway error: ${providerErr}]`));
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
catch (err) {
|
|
269
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
270
|
-
if (attempt < maxRetries) {
|
|
271
|
-
onProgress?.(`⚠ Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
272
|
-
await sleep(RETRY_DELAY_MS);
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
console.error(chalk.dim(`\n [Runway error: ${errMsg}]`));
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Generate video with retry logic for Veo (Gemini) provider
|
|
283
|
-
*/
|
|
284
|
-
export async function generateVideoWithRetryVeo(gemini, segment, options, maxRetries, onProgress) {
|
|
285
|
-
const prompt = segment.visualStyle
|
|
286
|
-
? `${segment.visuals}. Style: ${segment.visualStyle}`
|
|
287
|
-
: segment.visuals;
|
|
288
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
289
|
-
try {
|
|
290
|
-
const result = await gemini.generateVideo(prompt, {
|
|
291
|
-
prompt,
|
|
292
|
-
referenceImage: options.referenceImage,
|
|
293
|
-
duration: options.duration,
|
|
294
|
-
aspectRatio: options.aspectRatio,
|
|
295
|
-
model: "veo-3.1-fast-generate-preview",
|
|
296
|
-
});
|
|
297
|
-
if (result.status !== "failed" && result.id) {
|
|
298
|
-
return { operationName: result.id };
|
|
299
|
-
}
|
|
300
|
-
const providerErr = result.error || "Veo returned failed status";
|
|
301
|
-
if (attempt < maxRetries) {
|
|
302
|
-
onProgress?.(`⚠ ${providerErr.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
303
|
-
await sleep(RETRY_DELAY_MS);
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
console.error(chalk.dim(`\n [Veo error: ${providerErr}]`));
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
catch (err) {
|
|
310
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
311
|
-
if (attempt < maxRetries) {
|
|
312
|
-
onProgress?.(`⚠ Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
313
|
-
await sleep(RETRY_DELAY_MS);
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
console.error(chalk.dim(`\n [Veo error: ${errMsg}]`));
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
return null;
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Wait for video completion with retry logic
|
|
324
|
-
*/
|
|
325
|
-
export async function waitForVideoWithRetry(provider, taskId, providerType, maxRetries, onProgress, timeout) {
|
|
326
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
327
|
-
try {
|
|
328
|
-
let result;
|
|
329
|
-
if (providerType === "kling") {
|
|
330
|
-
result = await provider.waitForCompletion(taskId, "image2video", (status) => onProgress?.(status.status || "processing"), timeout || 600000);
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
result = await provider.waitForCompletion(taskId, (status) => {
|
|
334
|
-
const progress = status.progress !== undefined ? `${status.progress}%` : status.status;
|
|
335
|
-
onProgress?.(progress || "processing");
|
|
336
|
-
}, timeout || 300000);
|
|
337
|
-
}
|
|
338
|
-
if (result.status === "completed" && result.videoUrl) {
|
|
339
|
-
return { videoUrl: result.videoUrl };
|
|
340
|
-
}
|
|
341
|
-
// If failed, try resubmitting on next attempt
|
|
342
|
-
if (attempt < maxRetries) {
|
|
343
|
-
onProgress?.(`⚠ Failed, will need resubmission...`);
|
|
344
|
-
return null; // Signal need for resubmission
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
catch (err) {
|
|
348
|
-
if (attempt < maxRetries) {
|
|
349
|
-
onProgress?.(`⚠ Error waiting, retry ${attempt + 1}/${maxRetries}...`);
|
|
350
|
-
await sleep(RETRY_DELAY_MS);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Execute the full script-to-video pipeline programmatically.
|
|
358
|
-
*
|
|
359
|
-
* Pipeline stages:
|
|
360
|
-
* 1. Generate storyboard with Claude
|
|
361
|
-
* 2. Generate per-scene voiceovers with ElevenLabs TTS
|
|
362
|
-
* 3. Generate scene images (OpenAI/Gemini)
|
|
363
|
-
* 4. Generate scene videos (Kling/Runway) with extension to match narration
|
|
364
|
-
* 4.5. Apply text overlays if present in storyboard
|
|
365
|
-
* 5. Assemble .vibe.json project file
|
|
366
|
-
* 6. Optional AI review and auto-fix (Gemini)
|
|
367
|
-
*
|
|
368
|
-
* @param options - Pipeline configuration
|
|
369
|
-
* @returns Result with paths to all generated assets and project file
|
|
370
|
-
*/
|
|
371
|
-
export async function executeScriptToVideo(options) {
|
|
372
|
-
const outputDir = options.outputDir || "script-video-output";
|
|
373
|
-
try {
|
|
374
|
-
// Get storyboard provider API key
|
|
375
|
-
const storyboardProvider = options.storyboardProvider || "claude";
|
|
376
|
-
let storyboardApiKey;
|
|
377
|
-
if (storyboardProvider === "openai") {
|
|
378
|
-
storyboardApiKey = (await getApiKey("OPENAI_API_KEY", "OpenAI")) ?? undefined;
|
|
379
|
-
if (!storyboardApiKey) {
|
|
380
|
-
return { success: false, outputDir, scenes: 0, error: "OpenAI API key required for storyboard generation (--storyboard-provider openai). Run 'vibe setup' or set OPENAI_API_KEY in .env" };
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
else if (storyboardProvider === "gemini") {
|
|
384
|
-
storyboardApiKey = (await getApiKey("GOOGLE_API_KEY", "Google")) ?? undefined;
|
|
385
|
-
if (!storyboardApiKey) {
|
|
386
|
-
return { success: false, outputDir, scenes: 0, error: "Google API key required for storyboard generation (--storyboard-provider gemini). Run 'vibe setup' or set GOOGLE_API_KEY in .env" };
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
// Default: Claude
|
|
391
|
-
storyboardApiKey = (await getApiKey("ANTHROPIC_API_KEY", "Anthropic")) ?? undefined;
|
|
392
|
-
if (!storyboardApiKey) {
|
|
393
|
-
return { success: false, outputDir, scenes: 0, error: "Anthropic API key required for storyboard generation. Run 'vibe setup' or set ANTHROPIC_API_KEY in .env" };
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
// Get image provider API key
|
|
397
|
-
let imageApiKey;
|
|
398
|
-
const imageProvider = options.imageProvider || "openai";
|
|
399
|
-
if (imageProvider === "openai" || imageProvider === "dalle") {
|
|
400
|
-
imageApiKey = (await getApiKey("OPENAI_API_KEY", "OpenAI")) ?? undefined;
|
|
401
|
-
if (!imageApiKey) {
|
|
402
|
-
return { success: false, outputDir, scenes: 0, error: "OpenAI API key required for image generation. Run 'vibe setup' or set OPENAI_API_KEY in .env" };
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
else if (imageProvider === "gemini") {
|
|
406
|
-
imageApiKey = (await getApiKey("GOOGLE_API_KEY", "Google")) ?? undefined;
|
|
407
|
-
if (!imageApiKey) {
|
|
408
|
-
return { success: false, outputDir, scenes: 0, error: "Google API key required for Gemini image generation. Run 'vibe setup' or set GOOGLE_API_KEY in .env" };
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
else if (imageProvider === "grok") {
|
|
412
|
-
imageApiKey = (await getApiKey("XAI_API_KEY", "xAI")) ?? undefined;
|
|
413
|
-
if (!imageApiKey) {
|
|
414
|
-
return { success: false, outputDir, scenes: 0, error: "xAI API key required for Grok image generation. Run 'vibe setup' or set XAI_API_KEY in .env" };
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
let elevenlabsApiKey;
|
|
418
|
-
if (!options.noVoiceover) {
|
|
419
|
-
elevenlabsApiKey = (await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs")) ?? undefined;
|
|
420
|
-
if (!elevenlabsApiKey) {
|
|
421
|
-
return { success: false, outputDir, scenes: 0, error: "ElevenLabs API key required for voiceover (or use noVoiceover option). Run 'vibe setup' or set ELEVENLABS_API_KEY in .env" };
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
let videoApiKey;
|
|
425
|
-
if (!options.imagesOnly) {
|
|
426
|
-
const generatorKeyMap = {
|
|
427
|
-
grok: { envVar: "XAI_API_KEY", name: "xAI (Grok)" },
|
|
428
|
-
kling: { envVar: "KLING_API_KEY", name: "Kling" },
|
|
429
|
-
runway: { envVar: "RUNWAY_API_SECRET", name: "Runway" },
|
|
430
|
-
veo: { envVar: "GOOGLE_API_KEY", name: "Google (Veo)" },
|
|
431
|
-
};
|
|
432
|
-
const generator = options.generator || "grok";
|
|
433
|
-
const generatorInfo = generatorKeyMap[generator];
|
|
434
|
-
if (!generatorInfo) {
|
|
435
|
-
return { success: false, outputDir, scenes: 0, error: `Invalid generator: ${options.generator}. Available: ${Object.keys(generatorKeyMap).join(", ")}` };
|
|
436
|
-
}
|
|
437
|
-
videoApiKey = (await getApiKey(generatorInfo.envVar, generatorInfo.name)) ?? undefined;
|
|
438
|
-
if (!videoApiKey) {
|
|
439
|
-
return { success: false, outputDir, scenes: 0, error: `${generatorInfo.name} API key required (or use imagesOnly option). Run 'vibe setup' or set ${generatorInfo.envVar} in .env` };
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
// Create output directory
|
|
443
|
-
const absOutputDir = resolve(process.cwd(), outputDir);
|
|
444
|
-
if (!existsSync(absOutputDir)) {
|
|
445
|
-
await mkdir(absOutputDir, { recursive: true });
|
|
446
|
-
}
|
|
447
|
-
// Step 1: Generate storyboard
|
|
448
|
-
options.onProgress?.(`Analyzing script with ${storyboardProvider}...`);
|
|
449
|
-
let segments;
|
|
450
|
-
const creativityOpts = { creativity: options.creativity };
|
|
451
|
-
if (storyboardProvider === "openai") {
|
|
452
|
-
const openai = new OpenAIProvider();
|
|
453
|
-
await openai.initialize({ apiKey: storyboardApiKey });
|
|
454
|
-
segments = await openai.analyzeContent(options.script, options.duration, creativityOpts);
|
|
455
|
-
}
|
|
456
|
-
else if (storyboardProvider === "gemini") {
|
|
457
|
-
const gemini = new GeminiProvider();
|
|
458
|
-
await gemini.initialize({ apiKey: storyboardApiKey });
|
|
459
|
-
segments = await gemini.analyzeContent(options.script, options.duration, creativityOpts);
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
const claude = new ClaudeProvider();
|
|
463
|
-
await claude.initialize({ apiKey: storyboardApiKey });
|
|
464
|
-
segments = await claude.analyzeContent(options.script, options.duration, creativityOpts);
|
|
465
|
-
}
|
|
466
|
-
if (segments.length === 0) {
|
|
467
|
-
return { success: false, outputDir, scenes: 0, error: "Failed to generate storyboard" };
|
|
468
|
-
}
|
|
469
|
-
// Save storyboard as YAML (human-readable, easier to edit)
|
|
470
|
-
const storyboardPath = resolve(absOutputDir, "storyboard.yaml");
|
|
471
|
-
await writeFile(storyboardPath, yamlStringify({ scenes: segments }, { indent: 2 }), "utf-8");
|
|
472
|
-
const result = {
|
|
473
|
-
success: true,
|
|
474
|
-
outputDir: absOutputDir,
|
|
475
|
-
scenes: segments.length,
|
|
476
|
-
storyboardPath,
|
|
477
|
-
narrations: [],
|
|
478
|
-
narrationEntries: [],
|
|
479
|
-
images: [],
|
|
480
|
-
videos: [],
|
|
481
|
-
failedScenes: [],
|
|
482
|
-
failedNarrations: [],
|
|
483
|
-
};
|
|
484
|
-
// Step 2: Generate per-scene voiceovers with ElevenLabs
|
|
485
|
-
if (!options.noVoiceover && elevenlabsApiKey) {
|
|
486
|
-
options.onProgress?.(`Generating ${segments.length} voiceover(s) with ElevenLabs...`);
|
|
487
|
-
const elevenlabs = new ElevenLabsProvider();
|
|
488
|
-
await elevenlabs.initialize({ apiKey: elevenlabsApiKey });
|
|
489
|
-
for (let i = 0; i < segments.length; i++) {
|
|
490
|
-
const segment = segments[i];
|
|
491
|
-
const narrationText = segment.narration || segment.description;
|
|
492
|
-
if (!narrationText) {
|
|
493
|
-
// No narration text for this segment - add placeholder entry
|
|
494
|
-
result.narrationEntries.push({
|
|
495
|
-
path: null,
|
|
496
|
-
duration: segment.duration,
|
|
497
|
-
segmentIndex: i,
|
|
498
|
-
failed: false, // Not failed, just no text
|
|
499
|
-
});
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
// Pre-TTS word-count heuristic (warn only; narration may still fit)
|
|
503
|
-
const wordCount = narrationText.split(/\s+/).filter(Boolean).length;
|
|
504
|
-
const maxWords = segment.duration > 5 ? 24 : 12;
|
|
505
|
-
if (wordCount > maxWords * 1.3) {
|
|
506
|
-
options.onProgress?.(`⚠ Scene ${i + 1} narration has ${wordCount} words (target ~${maxWords} for ${segment.duration}s); speech may rush.`);
|
|
507
|
-
}
|
|
508
|
-
options.onProgress?.(`Scene ${i + 1}/${segments.length}: generating narration...`);
|
|
509
|
-
let ttsResult = await elevenlabs.textToSpeech(narrationText, {
|
|
510
|
-
voiceId: options.voice,
|
|
511
|
-
});
|
|
512
|
-
if (ttsResult.success && ttsResult.audioBuffer) {
|
|
513
|
-
const audioPath = resolve(absOutputDir, `narration-${i + 1}.mp3`);
|
|
514
|
-
await writeFile(audioPath, ttsResult.audioBuffer);
|
|
515
|
-
// Get actual audio duration
|
|
516
|
-
let actualDuration = await getAudioDuration(audioPath);
|
|
517
|
-
// Auto speed-adjust if narration exceeds video bracket (5s or 10s).
|
|
518
|
-
// Pick the bracket from the pre-TTS segment.duration (the storyboard
|
|
519
|
-
// estimate), not actualDuration — actualDuration IS the overage.
|
|
520
|
-
const videoBracket = segment.duration > 5 ? 10 : 5;
|
|
521
|
-
const overageRatio = actualDuration / videoBracket;
|
|
522
|
-
if (overageRatio > 1.0 && overageRatio <= 1.35) {
|
|
523
|
-
const adjustedSpeed = Math.min(1.35, parseFloat(overageRatio.toFixed(2)));
|
|
524
|
-
options.onProgress?.(`Scene ${i + 1}: adjusting narration speed to ${adjustedSpeed}x...`);
|
|
525
|
-
const speedResult = await elevenlabs.textToSpeech(narrationText, {
|
|
526
|
-
voiceId: options.voice,
|
|
527
|
-
speed: adjustedSpeed,
|
|
528
|
-
});
|
|
529
|
-
if (speedResult.success && speedResult.audioBuffer) {
|
|
530
|
-
await writeFile(audioPath, speedResult.audioBuffer);
|
|
531
|
-
actualDuration = await getAudioDuration(audioPath);
|
|
532
|
-
ttsResult = speedResult;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
else if (overageRatio > 1.35) {
|
|
536
|
-
options.onProgress?.(`⚠ Scene ${i + 1} narration is ${((overageRatio - 1) * 100).toFixed(0)}% over target (${actualDuration.toFixed(1)}s vs ${videoBracket}s bracket)`);
|
|
537
|
-
}
|
|
538
|
-
segment.duration = actualDuration;
|
|
539
|
-
// Add to both arrays for backwards compatibility
|
|
540
|
-
result.narrations.push(audioPath);
|
|
541
|
-
result.narrationEntries.push({
|
|
542
|
-
path: audioPath,
|
|
543
|
-
duration: actualDuration,
|
|
544
|
-
segmentIndex: i,
|
|
545
|
-
failed: false,
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
// TTS failed - add placeholder entry with error info
|
|
550
|
-
result.narrationEntries.push({
|
|
551
|
-
path: null,
|
|
552
|
-
duration: segment.duration, // Keep original estimated duration
|
|
553
|
-
segmentIndex: i,
|
|
554
|
-
failed: true,
|
|
555
|
-
error: ttsResult.error || "Unknown TTS error",
|
|
556
|
-
});
|
|
557
|
-
result.failedNarrations.push(i + 1); // 1-indexed for user display
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
// Recalculate startTime for all segments
|
|
561
|
-
let currentTime = 0;
|
|
562
|
-
for (const segment of segments) {
|
|
563
|
-
segment.startTime = currentTime;
|
|
564
|
-
currentTime += segment.duration;
|
|
565
|
-
}
|
|
566
|
-
// Re-save storyboard with updated durations
|
|
567
|
-
await writeFile(storyboardPath, yamlStringify({ scenes: segments }, { indent: 2 }), "utf-8");
|
|
568
|
-
}
|
|
569
|
-
// Step 3: Generate images
|
|
570
|
-
options.onProgress?.(`Generating ${segments.length} scene image(s) with ${imageProvider}...`);
|
|
571
|
-
const dalleImageSizes = {
|
|
572
|
-
"16:9": "1536x1024",
|
|
573
|
-
"9:16": "1024x1536",
|
|
574
|
-
"1:1": "1024x1024",
|
|
575
|
-
};
|
|
576
|
-
let openaiImageInstance;
|
|
577
|
-
let geminiInstance;
|
|
578
|
-
let grokInstance;
|
|
579
|
-
if (imageProvider === "openai" || imageProvider === "dalle") {
|
|
580
|
-
openaiImageInstance = new OpenAIImageProvider();
|
|
581
|
-
await openaiImageInstance.initialize({ apiKey: imageApiKey });
|
|
582
|
-
}
|
|
583
|
-
else if (imageProvider === "gemini") {
|
|
584
|
-
geminiInstance = new GeminiProvider();
|
|
585
|
-
await geminiInstance.initialize({ apiKey: imageApiKey });
|
|
586
|
-
}
|
|
587
|
-
else if (imageProvider === "grok") {
|
|
588
|
-
grokInstance = new GrokProvider();
|
|
589
|
-
await grokInstance.initialize({ apiKey: imageApiKey });
|
|
590
|
-
}
|
|
591
|
-
const imagePaths = [];
|
|
592
|
-
for (let i = 0; i < segments.length; i++) {
|
|
593
|
-
const segment = segments[i];
|
|
594
|
-
const imagePrompt = segment.visualStyle
|
|
595
|
-
? `${segment.visuals}. Style: ${segment.visualStyle}`
|
|
596
|
-
: segment.visuals;
|
|
597
|
-
options.onProgress?.(`Scene ${i + 1}/${segments.length}: generating image...`);
|
|
598
|
-
try {
|
|
599
|
-
let imageBuffer;
|
|
600
|
-
let imageUrl;
|
|
601
|
-
if ((imageProvider === "openai" || imageProvider === "dalle") && openaiImageInstance) {
|
|
602
|
-
const imageResult = await openaiImageInstance.generateImage(imagePrompt, {
|
|
603
|
-
size: dalleImageSizes[options.aspectRatio || "16:9"] || "1536x1024",
|
|
604
|
-
quality: "standard",
|
|
605
|
-
});
|
|
606
|
-
if (imageResult.success && imageResult.images && imageResult.images.length > 0) {
|
|
607
|
-
// GPT Image 1.5 returns base64, DALL-E 3 returns URL
|
|
608
|
-
const img = imageResult.images[0];
|
|
609
|
-
if (img.base64) {
|
|
610
|
-
imageBuffer = Buffer.from(img.base64, "base64");
|
|
611
|
-
}
|
|
612
|
-
else if (img.url) {
|
|
613
|
-
imageUrl = img.url;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
// else: imageResult.error is available but not captured
|
|
617
|
-
}
|
|
618
|
-
else if (imageProvider === "gemini" && geminiInstance) {
|
|
619
|
-
const imageResult = await geminiInstance.generateImage(imagePrompt, {
|
|
620
|
-
aspectRatio: (options.aspectRatio || "16:9"),
|
|
621
|
-
});
|
|
622
|
-
if (imageResult.success && imageResult.images?.[0]?.base64) {
|
|
623
|
-
imageBuffer = Buffer.from(imageResult.images[0].base64, "base64");
|
|
624
|
-
}
|
|
625
|
-
// else: imageResult.error is available but not captured
|
|
626
|
-
}
|
|
627
|
-
else if (imageProvider === "grok" && grokInstance) {
|
|
628
|
-
const imageResult = await grokInstance.generateImage(imagePrompt, {
|
|
629
|
-
aspectRatio: options.aspectRatio || "16:9",
|
|
630
|
-
});
|
|
631
|
-
if (imageResult.success && imageResult.images && imageResult.images.length > 0) {
|
|
632
|
-
const img = imageResult.images[0];
|
|
633
|
-
if (img.base64) {
|
|
634
|
-
imageBuffer = Buffer.from(img.base64, "base64");
|
|
635
|
-
}
|
|
636
|
-
else if (img.url) {
|
|
637
|
-
imageUrl = img.url;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
const imagePath = resolve(absOutputDir, `scene-${i + 1}.png`);
|
|
642
|
-
if (imageBuffer) {
|
|
643
|
-
await writeFile(imagePath, imageBuffer);
|
|
644
|
-
imagePaths.push(imagePath);
|
|
645
|
-
result.images.push(imagePath);
|
|
646
|
-
}
|
|
647
|
-
else if (imageUrl) {
|
|
648
|
-
const response = await fetch(imageUrl);
|
|
649
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
650
|
-
await writeFile(imagePath, buffer);
|
|
651
|
-
imagePaths.push(imagePath);
|
|
652
|
-
result.images.push(imagePath);
|
|
653
|
-
}
|
|
654
|
-
else {
|
|
655
|
-
// Track failed scene - error details not captured (see provider imageResult.error)
|
|
656
|
-
// The failedScenes array tracks which scenes failed for the caller
|
|
657
|
-
imagePaths.push("");
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
catch {
|
|
661
|
-
imagePaths.push("");
|
|
662
|
-
}
|
|
663
|
-
// Rate limiting delay
|
|
664
|
-
if (i < segments.length - 1) {
|
|
665
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
// Step 4 (alt): branch into scene-project emit when --format scenes.
|
|
669
|
-
// We have everything needed: storyboard, narrations, images. Video gen,
|
|
670
|
-
// text overlays, .vibe.json assembly, and review are all skipped — the
|
|
671
|
-
// user iterates on the resulting HTML scenes via `vibe scene lint/render`.
|
|
672
|
-
if (options.format === "scenes") {
|
|
673
|
-
const { executeSegmentsToScenes } = await import("./_shared/segments-to-scenes.js");
|
|
674
|
-
options.onProgress?.("Materialising scene project...");
|
|
675
|
-
const sceneAspect = (options.aspectRatio === "1:1" || options.aspectRatio === "16:9" || options.aspectRatio === "9:16")
|
|
676
|
-
? options.aspectRatio
|
|
677
|
-
: "16:9";
|
|
678
|
-
const scenes = await executeSegmentsToScenes({
|
|
679
|
-
segments,
|
|
680
|
-
narrationEntries: result.narrationEntries,
|
|
681
|
-
imagePaths,
|
|
682
|
-
outputDir: absOutputDir,
|
|
683
|
-
aspectRatio: sceneAspect,
|
|
684
|
-
scenePreset: options.scenePreset,
|
|
685
|
-
onProgress: options.onProgress,
|
|
686
|
-
});
|
|
687
|
-
result.format = "scenes";
|
|
688
|
-
result.scenePaths = scenes.scenePaths;
|
|
689
|
-
result.sceneLint = scenes.lintResult;
|
|
690
|
-
result.totalDuration = segments.reduce((sum, s) => sum + s.duration, 0);
|
|
691
|
-
if (!scenes.success) {
|
|
692
|
-
return { ...result, success: false, error: scenes.error ?? "Scene emit failed" };
|
|
693
|
-
}
|
|
694
|
-
return result;
|
|
695
|
-
}
|
|
696
|
-
// Step 4: Generate videos (if not images-only)
|
|
697
|
-
const videoPaths = [];
|
|
698
|
-
const maxRetries = options.retries ?? DEFAULT_VIDEO_RETRIES;
|
|
699
|
-
if (!options.imagesOnly && videoApiKey) {
|
|
700
|
-
options.onProgress?.(`Generating ${segments.length} scene video(s) with ${options.generator || "grok"}...`);
|
|
701
|
-
if (options.generator === "grok") {
|
|
702
|
-
const grok = new GrokProvider();
|
|
703
|
-
await grok.initialize({ apiKey: videoApiKey });
|
|
704
|
-
for (let i = 0; i < segments.length; i++) {
|
|
705
|
-
if (!imagePaths[i]) {
|
|
706
|
-
videoPaths.push("");
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
const segment = segments[i];
|
|
710
|
-
const videoDuration = Math.min(15, Math.max(1, segment.duration));
|
|
711
|
-
options.onProgress?.(`Scene ${i + 1}/${segments.length}: generating video (grok)...`);
|
|
712
|
-
// Image-to-video: encode image as data URI
|
|
713
|
-
const imageBuffer = await readFile(imagePaths[i]);
|
|
714
|
-
const ext = extname(imagePaths[i]).toLowerCase().slice(1);
|
|
715
|
-
const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
|
716
|
-
const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
|
|
717
|
-
const taskResult = await generateVideoWithRetryGrok(grok, segment, { duration: videoDuration, aspectRatio: (options.aspectRatio || "16:9"), referenceImage }, maxRetries);
|
|
718
|
-
if (taskResult) {
|
|
719
|
-
try {
|
|
720
|
-
const waitResult = await grok.waitForCompletion(taskResult.requestId, undefined, 300000);
|
|
721
|
-
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
722
|
-
const videoPath = resolve(absOutputDir, `scene-${i + 1}.mp4`);
|
|
723
|
-
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
724
|
-
await writeFile(videoPath, buffer);
|
|
725
|
-
const targetDuration = segment.duration;
|
|
726
|
-
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
727
|
-
if (actualVideoDuration < targetDuration - 0.1) {
|
|
728
|
-
const extendedPath = resolve(absOutputDir, `scene-${i + 1}-extended.mp4`);
|
|
729
|
-
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
730
|
-
await unlink(videoPath);
|
|
731
|
-
await rename(extendedPath, videoPath);
|
|
732
|
-
}
|
|
733
|
-
videoPaths.push(videoPath);
|
|
734
|
-
result.videos.push(videoPath);
|
|
735
|
-
}
|
|
736
|
-
else {
|
|
737
|
-
logSceneFailure("Grok", `scene ${i + 1}`, waitResult);
|
|
738
|
-
videoPaths.push("");
|
|
739
|
-
result.failedScenes.push(i + 1);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
catch (err) {
|
|
743
|
-
logSceneFailure("Grok", `scene ${i + 1}`, err);
|
|
744
|
-
videoPaths.push("");
|
|
745
|
-
result.failedScenes.push(i + 1);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
else {
|
|
749
|
-
videoPaths.push("");
|
|
750
|
-
result.failedScenes.push(i + 1);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
else if (options.generator === "kling") {
|
|
755
|
-
const kling = new KlingProvider();
|
|
756
|
-
await kling.initialize({ apiKey: videoApiKey });
|
|
757
|
-
if (!kling.isConfigured()) {
|
|
758
|
-
return { success: false, outputDir: absOutputDir, scenes: segments.length, error: "Invalid Kling API key format. Use ACCESS_KEY:SECRET_KEY" };
|
|
759
|
-
}
|
|
760
|
-
// Kling image2video requires a public image URL (not base64). When an
|
|
761
|
-
// ImgBB key is configured, upload each scene image once and pass the
|
|
762
|
-
// URL as referenceImage; otherwise fall through to text2video.
|
|
763
|
-
const imgbbApiKey = (await getApiKeyFromConfig("imgbb")) || process.env.IMGBB_API_KEY;
|
|
764
|
-
const imageUrls = new Array(segments.length);
|
|
765
|
-
if (imgbbApiKey) {
|
|
766
|
-
options.onProgress?.("Uploading scene images for Kling image-to-video...");
|
|
767
|
-
for (let i = 0; i < imagePaths.length; i++) {
|
|
768
|
-
if (imagePaths[i]) {
|
|
769
|
-
try {
|
|
770
|
-
const imageBuffer = await readFile(imagePaths[i]);
|
|
771
|
-
const uploadResult = await uploadToImgbb(imageBuffer, imgbbApiKey);
|
|
772
|
-
if (uploadResult.success && uploadResult.url) {
|
|
773
|
-
imageUrls[i] = uploadResult.url;
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
catch {
|
|
777
|
-
imageUrls[i] = undefined;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
for (let i = 0; i < segments.length; i++) {
|
|
783
|
-
if (!imagePaths[i]) {
|
|
784
|
-
videoPaths.push("");
|
|
785
|
-
continue;
|
|
786
|
-
}
|
|
787
|
-
const segment = segments[i];
|
|
788
|
-
const videoDuration = (segment.duration > 5 ? 10 : 5);
|
|
789
|
-
options.onProgress?.(`Scene ${i + 1}/${segments.length}: generating video (kling)...`);
|
|
790
|
-
const taskResult = await generateVideoWithRetryKling(kling, segment, {
|
|
791
|
-
duration: videoDuration,
|
|
792
|
-
aspectRatio: (options.aspectRatio || "16:9"),
|
|
793
|
-
referenceImage: imageUrls[i],
|
|
794
|
-
}, maxRetries);
|
|
795
|
-
if (taskResult) {
|
|
796
|
-
try {
|
|
797
|
-
const waitResult = await kling.waitForCompletion(taskResult.taskId, taskResult.type, undefined, 600000);
|
|
798
|
-
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
799
|
-
const videoPath = resolve(absOutputDir, `scene-${i + 1}.mp4`);
|
|
800
|
-
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
801
|
-
await writeFile(videoPath, buffer);
|
|
802
|
-
// Extend video to match narration duration. Use extendVideoToTarget
|
|
803
|
-
// so large gaps (ratio > 1.4) can use Kling's own extend API for
|
|
804
|
-
// natural continuation instead of freeze-frame padding.
|
|
805
|
-
await extendVideoToTarget(videoPath, segment.duration, absOutputDir, `Scene ${i + 1}`, {
|
|
806
|
-
kling,
|
|
807
|
-
videoId: waitResult.videoId,
|
|
808
|
-
onProgress: options.onProgress,
|
|
809
|
-
});
|
|
810
|
-
videoPaths.push(videoPath);
|
|
811
|
-
result.videos.push(videoPath);
|
|
812
|
-
}
|
|
813
|
-
else {
|
|
814
|
-
logSceneFailure("Kling", `scene ${i + 1}`, waitResult);
|
|
815
|
-
videoPaths.push("");
|
|
816
|
-
result.failedScenes.push(i + 1);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
catch (err) {
|
|
820
|
-
logSceneFailure("Kling", `scene ${i + 1}`, err);
|
|
821
|
-
videoPaths.push("");
|
|
822
|
-
result.failedScenes.push(i + 1);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
else {
|
|
826
|
-
videoPaths.push("");
|
|
827
|
-
result.failedScenes.push(i + 1);
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
else if (options.generator === "veo") {
|
|
832
|
-
// Veo (Gemini)
|
|
833
|
-
const veo = new GeminiProvider();
|
|
834
|
-
await veo.initialize({ apiKey: videoApiKey });
|
|
835
|
-
for (let i = 0; i < segments.length; i++) {
|
|
836
|
-
if (!imagePaths[i]) {
|
|
837
|
-
videoPaths.push("");
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
const segment = segments[i];
|
|
841
|
-
const veoDuration = (segment.duration > 6 ? 8 : segment.duration > 4 ? 6 : 4);
|
|
842
|
-
options.onProgress?.(`Scene ${i + 1}/${segments.length}: generating video (veo)...`);
|
|
843
|
-
const taskResult = await generateVideoWithRetryVeo(veo, segment, { duration: veoDuration, aspectRatio: (options.aspectRatio || "16:9") }, maxRetries);
|
|
844
|
-
if (taskResult) {
|
|
845
|
-
try {
|
|
846
|
-
const waitResult = await veo.waitForVideoCompletion(taskResult.operationName, undefined, 300000);
|
|
847
|
-
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
848
|
-
const videoPath = resolve(absOutputDir, `scene-${i + 1}.mp4`);
|
|
849
|
-
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
850
|
-
await writeFile(videoPath, buffer);
|
|
851
|
-
// Extend video to match narration duration if needed
|
|
852
|
-
const targetDuration = segment.duration;
|
|
853
|
-
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
854
|
-
if (actualVideoDuration < targetDuration - 0.1) {
|
|
855
|
-
const extendedPath = resolve(absOutputDir, `scene-${i + 1}-extended.mp4`);
|
|
856
|
-
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
857
|
-
await unlink(videoPath);
|
|
858
|
-
await rename(extendedPath, videoPath);
|
|
859
|
-
}
|
|
860
|
-
videoPaths.push(videoPath);
|
|
861
|
-
result.videos.push(videoPath);
|
|
862
|
-
}
|
|
863
|
-
else {
|
|
864
|
-
logSceneFailure("Veo", `scene ${i + 1}`, waitResult);
|
|
865
|
-
videoPaths.push("");
|
|
866
|
-
result.failedScenes.push(i + 1);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
catch (err) {
|
|
870
|
-
logSceneFailure("Veo", `scene ${i + 1}`, err);
|
|
871
|
-
videoPaths.push("");
|
|
872
|
-
result.failedScenes.push(i + 1);
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
else {
|
|
876
|
-
videoPaths.push("");
|
|
877
|
-
result.failedScenes.push(i + 1);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
else {
|
|
882
|
-
// Runway
|
|
883
|
-
const runway = new RunwayProvider();
|
|
884
|
-
await runway.initialize({ apiKey: videoApiKey });
|
|
885
|
-
for (let i = 0; i < segments.length; i++) {
|
|
886
|
-
if (!imagePaths[i]) {
|
|
887
|
-
videoPaths.push("");
|
|
888
|
-
continue;
|
|
889
|
-
}
|
|
890
|
-
const segment = segments[i];
|
|
891
|
-
const imageBuffer = await readFile(imagePaths[i]);
|
|
892
|
-
const ext = extname(imagePaths[i]).toLowerCase().slice(1);
|
|
893
|
-
const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
|
894
|
-
const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
|
|
895
|
-
const videoDuration = (segment.duration > 5 ? 10 : 5);
|
|
896
|
-
const aspectRatio = options.aspectRatio === "1:1" ? "16:9" : (options.aspectRatio || "16:9");
|
|
897
|
-
options.onProgress?.(`Scene ${i + 1}/${segments.length}: generating video (runway)...`);
|
|
898
|
-
const taskResult = await generateVideoWithRetryRunway(runway, segment, referenceImage, { duration: videoDuration, aspectRatio }, maxRetries);
|
|
899
|
-
if (taskResult) {
|
|
900
|
-
try {
|
|
901
|
-
const waitResult = await runway.waitForCompletion(taskResult.taskId, undefined, 300000);
|
|
902
|
-
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
903
|
-
const videoPath = resolve(absOutputDir, `scene-${i + 1}.mp4`);
|
|
904
|
-
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
905
|
-
await writeFile(videoPath, buffer);
|
|
906
|
-
// Extend video to match narration duration if needed
|
|
907
|
-
const targetDuration = segment.duration;
|
|
908
|
-
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
909
|
-
if (actualVideoDuration < targetDuration - 0.1) {
|
|
910
|
-
const extendedPath = resolve(absOutputDir, `scene-${i + 1}-extended.mp4`);
|
|
911
|
-
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
912
|
-
await unlink(videoPath);
|
|
913
|
-
await rename(extendedPath, videoPath);
|
|
914
|
-
}
|
|
915
|
-
videoPaths.push(videoPath);
|
|
916
|
-
result.videos.push(videoPath);
|
|
917
|
-
}
|
|
918
|
-
else {
|
|
919
|
-
logSceneFailure("Runway", `scene ${i + 1}`, waitResult);
|
|
920
|
-
videoPaths.push("");
|
|
921
|
-
result.failedScenes.push(i + 1);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
catch (err) {
|
|
925
|
-
logSceneFailure("Runway", `scene ${i + 1}`, err);
|
|
926
|
-
videoPaths.push("");
|
|
927
|
-
result.failedScenes.push(i + 1);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
else {
|
|
931
|
-
videoPaths.push("");
|
|
932
|
-
result.failedScenes.push(i + 1);
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
// Step 4.5: Apply text overlays (if segments have textOverlays)
|
|
938
|
-
if (!options.noTextOverlay) {
|
|
939
|
-
const overlayCount = segments.filter((s, i) => s.textOverlays?.length && videoPaths[i]).length;
|
|
940
|
-
if (overlayCount > 0)
|
|
941
|
-
options.onProgress?.(`Applying text overlays to ${overlayCount} scene(s)...`);
|
|
942
|
-
for (let i = 0; i < segments.length; i++) {
|
|
943
|
-
const segment = segments[i];
|
|
944
|
-
if (segment.textOverlays && segment.textOverlays.length > 0 && videoPaths[i] && videoPaths[i] !== "") {
|
|
945
|
-
try {
|
|
946
|
-
const overlayOutput = videoPaths[i].replace(/(\.[^.]+)$/, "-overlay$1");
|
|
947
|
-
const overlayResult = await applyTextOverlays({
|
|
948
|
-
videoPath: videoPaths[i],
|
|
949
|
-
texts: segment.textOverlays,
|
|
950
|
-
outputPath: overlayOutput,
|
|
951
|
-
style: options.textStyle || "lower-third",
|
|
952
|
-
});
|
|
953
|
-
if (overlayResult.success && overlayResult.outputPath) {
|
|
954
|
-
videoPaths[i] = overlayResult.outputPath;
|
|
955
|
-
}
|
|
956
|
-
// Silent fallback: keep original on failure
|
|
957
|
-
}
|
|
958
|
-
catch {
|
|
959
|
-
// Silent fallback: keep original video
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
// Step 5: Create project file
|
|
965
|
-
options.onProgress?.("Assembling project...");
|
|
966
|
-
const project = new Project("Script-to-Video Output");
|
|
967
|
-
project.setAspectRatio((options.aspectRatio || "16:9"));
|
|
968
|
-
// Clear default tracks
|
|
969
|
-
const defaultTracks = project.getTracks();
|
|
970
|
-
for (const track of defaultTracks) {
|
|
971
|
-
project.removeTrack(track.id);
|
|
972
|
-
}
|
|
973
|
-
const videoTrack = project.addTrack({
|
|
974
|
-
name: "Video",
|
|
975
|
-
type: "video",
|
|
976
|
-
order: 1,
|
|
977
|
-
isMuted: false,
|
|
978
|
-
isLocked: false,
|
|
979
|
-
isVisible: true,
|
|
980
|
-
});
|
|
981
|
-
const audioTrack = project.addTrack({
|
|
982
|
-
name: "Audio",
|
|
983
|
-
type: "audio",
|
|
984
|
-
order: 0,
|
|
985
|
-
isMuted: false,
|
|
986
|
-
isLocked: false,
|
|
987
|
-
isVisible: true,
|
|
988
|
-
});
|
|
989
|
-
// Add narration clips - use narrationEntries for proper segment alignment
|
|
990
|
-
if (result.narrationEntries && result.narrationEntries.length > 0) {
|
|
991
|
-
for (const entry of result.narrationEntries) {
|
|
992
|
-
// Skip failed or missing narrations
|
|
993
|
-
if (entry.failed || !entry.path)
|
|
994
|
-
continue;
|
|
995
|
-
const segment = segments[entry.segmentIndex];
|
|
996
|
-
const narrationDuration = await getAudioDuration(entry.path);
|
|
997
|
-
const audioSource = project.addSource({
|
|
998
|
-
name: `Narration ${entry.segmentIndex + 1}`,
|
|
999
|
-
url: entry.path,
|
|
1000
|
-
type: "audio",
|
|
1001
|
-
duration: narrationDuration,
|
|
1002
|
-
});
|
|
1003
|
-
project.addClip({
|
|
1004
|
-
sourceId: audioSource.id,
|
|
1005
|
-
trackId: audioTrack.id,
|
|
1006
|
-
startTime: segment.startTime,
|
|
1007
|
-
duration: narrationDuration,
|
|
1008
|
-
sourceStartOffset: 0,
|
|
1009
|
-
sourceEndOffset: narrationDuration,
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
// Add video/image clips
|
|
1014
|
-
let currentTime = 0;
|
|
1015
|
-
for (let i = 0; i < segments.length; i++) {
|
|
1016
|
-
const segment = segments[i];
|
|
1017
|
-
const hasVideo = videoPaths[i] && videoPaths[i] !== "";
|
|
1018
|
-
const hasImage = imagePaths[i] && imagePaths[i] !== "";
|
|
1019
|
-
if (!hasVideo && !hasImage) {
|
|
1020
|
-
currentTime += segment.duration;
|
|
1021
|
-
continue;
|
|
1022
|
-
}
|
|
1023
|
-
const assetPath = hasVideo ? videoPaths[i] : imagePaths[i];
|
|
1024
|
-
const mediaType = hasVideo ? "video" : "image";
|
|
1025
|
-
// Use actual video duration (after extension) instead of segment.duration
|
|
1026
|
-
const actualDuration = hasVideo
|
|
1027
|
-
? await getVideoDuration(assetPath)
|
|
1028
|
-
: segment.duration;
|
|
1029
|
-
const source = project.addSource({
|
|
1030
|
-
name: `Scene ${i + 1}`,
|
|
1031
|
-
url: assetPath,
|
|
1032
|
-
type: mediaType,
|
|
1033
|
-
duration: actualDuration,
|
|
1034
|
-
});
|
|
1035
|
-
project.addClip({
|
|
1036
|
-
sourceId: source.id,
|
|
1037
|
-
trackId: videoTrack.id,
|
|
1038
|
-
startTime: currentTime,
|
|
1039
|
-
duration: actualDuration,
|
|
1040
|
-
sourceStartOffset: 0,
|
|
1041
|
-
sourceEndOffset: actualDuration,
|
|
1042
|
-
});
|
|
1043
|
-
currentTime += actualDuration;
|
|
1044
|
-
}
|
|
1045
|
-
// Save project file. Default is `{outputDir}/project.vibe.json`; callers
|
|
1046
|
-
// can override via projectFilePath (used by the CLI's `-o` handling).
|
|
1047
|
-
const projectPath = options.projectFilePath
|
|
1048
|
-
? resolve(process.cwd(), options.projectFilePath)
|
|
1049
|
-
: resolve(absOutputDir, "project.vibe.json");
|
|
1050
|
-
const projectParentDir = dirname(projectPath);
|
|
1051
|
-
if (!existsSync(projectParentDir)) {
|
|
1052
|
-
await mkdir(projectParentDir, { recursive: true });
|
|
1053
|
-
}
|
|
1054
|
-
await writeFile(projectPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
|
|
1055
|
-
result.projectPath = projectPath;
|
|
1056
|
-
result.totalDuration = currentTime;
|
|
1057
|
-
// Step 6: AI Review & Auto-fix (optional, --review flag)
|
|
1058
|
-
if (options.review) {
|
|
1059
|
-
options.onProgress?.("Reviewing video with Gemini AI...");
|
|
1060
|
-
try {
|
|
1061
|
-
// Look for storyboard file (YAML preferred, JSON fallback for backward compat)
|
|
1062
|
-
const storyboardYaml = resolve(absOutputDir, "storyboard.yaml");
|
|
1063
|
-
const storyboardJson = resolve(absOutputDir, "storyboard.json");
|
|
1064
|
-
const storyboardFile = existsSync(storyboardYaml) ? storyboardYaml : existsSync(storyboardJson) ? storyboardJson : undefined;
|
|
1065
|
-
// Export project to temp MP4 for review (use first valid video as proxy)
|
|
1066
|
-
const reviewTarget = videoPaths.find((p) => p && p !== "") || imagePaths.find((p) => p && p !== "");
|
|
1067
|
-
if (reviewTarget) {
|
|
1068
|
-
const reviewResult = await executeReview({
|
|
1069
|
-
videoPath: reviewTarget,
|
|
1070
|
-
storyboardPath: storyboardFile,
|
|
1071
|
-
autoApply: options.reviewAutoApply,
|
|
1072
|
-
model: "flash",
|
|
1073
|
-
});
|
|
1074
|
-
if (reviewResult.success) {
|
|
1075
|
-
result.reviewFeedback = reviewResult.feedback;
|
|
1076
|
-
result.appliedFixes = reviewResult.appliedFixes;
|
|
1077
|
-
result.reviewedVideoPath = reviewResult.outputPath;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
catch {
|
|
1082
|
-
// Review is non-critical, continue with result
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
return result;
|
|
1086
|
-
}
|
|
1087
|
-
catch (error) {
|
|
1088
|
-
return {
|
|
1089
|
-
success: false,
|
|
1090
|
-
outputDir,
|
|
1091
|
-
scenes: 0,
|
|
1092
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1093
|
-
};
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
/**
|
|
1097
|
-
* Regenerate specific scene(s) in an existing script-to-video project.
|
|
1098
|
-
*
|
|
1099
|
-
* Reads the storyboard.json from the project directory, then regenerates
|
|
1100
|
-
* the requested scenes using the specified video/image provider. Supports
|
|
1101
|
-
* image-to-video via ImgBB URL upload for Kling.
|
|
1102
|
-
*
|
|
1103
|
-
* @param options - Scene regeneration configuration
|
|
1104
|
-
* @returns Result with lists of regenerated and failed scene numbers
|
|
1105
|
-
*/
|
|
1106
|
-
export async function executeRegenerateScene(options) {
|
|
1107
|
-
const result = {
|
|
1108
|
-
success: false,
|
|
1109
|
-
regeneratedScenes: [],
|
|
1110
|
-
failedScenes: [],
|
|
1111
|
-
};
|
|
1112
|
-
try {
|
|
1113
|
-
const outputDir = resolve(process.cwd(), options.projectDir);
|
|
1114
|
-
if (!existsSync(outputDir)) {
|
|
1115
|
-
return { ...result, error: `Project directory not found: ${outputDir}` };
|
|
1116
|
-
}
|
|
1117
|
-
// Load storyboard (YAML preferred, JSON fallback for backward compat)
|
|
1118
|
-
const yamlPath = resolve(outputDir, "storyboard.yaml");
|
|
1119
|
-
const jsonPath = resolve(outputDir, "storyboard.json");
|
|
1120
|
-
const storyboardPath = existsSync(yamlPath) ? yamlPath : existsSync(jsonPath) ? jsonPath : null;
|
|
1121
|
-
if (!storyboardPath) {
|
|
1122
|
-
return { ...result, error: `Storyboard not found in: ${outputDir} (expected storyboard.yaml or storyboard.json)` };
|
|
1123
|
-
}
|
|
1124
|
-
const storyboardContent = await readFile(storyboardPath, "utf-8");
|
|
1125
|
-
const segments = storyboardPath.endsWith(".yaml")
|
|
1126
|
-
? yamlParse(storyboardContent).scenes
|
|
1127
|
-
: JSON.parse(storyboardContent);
|
|
1128
|
-
// Validate scenes
|
|
1129
|
-
for (const sceneNum of options.scenes) {
|
|
1130
|
-
if (sceneNum < 1 || sceneNum > segments.length) {
|
|
1131
|
-
return { ...result, error: `Scene ${sceneNum} does not exist. Storyboard has ${segments.length} scenes.` };
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
const regenerateVideo = options.videoOnly || (!options.narrationOnly && !options.imageOnly);
|
|
1135
|
-
const regenerateNarration = options.narrationOnly || (!options.videoOnly && !options.imageOnly);
|
|
1136
|
-
const regenerateImage = options.imageOnly || (!options.videoOnly && !options.narrationOnly);
|
|
1137
|
-
// Get API keys
|
|
1138
|
-
let videoApiKey;
|
|
1139
|
-
if (regenerateVideo) {
|
|
1140
|
-
const generatorKeyMap = {
|
|
1141
|
-
grok: { envVar: "XAI_API_KEY", name: "xAI (Grok)" },
|
|
1142
|
-
kling: { envVar: "KLING_API_KEY", name: "Kling" },
|
|
1143
|
-
runway: { envVar: "RUNWAY_API_SECRET", name: "Runway" },
|
|
1144
|
-
veo: { envVar: "GOOGLE_API_KEY", name: "Google (Veo)" },
|
|
1145
|
-
};
|
|
1146
|
-
const generator = options.generator || "grok";
|
|
1147
|
-
const genInfo = generatorKeyMap[generator];
|
|
1148
|
-
if (!genInfo) {
|
|
1149
|
-
return { ...result, error: `Invalid generator: ${generator}. Available: ${Object.keys(generatorKeyMap).join(", ")}` };
|
|
1150
|
-
}
|
|
1151
|
-
videoApiKey = (await getApiKey(genInfo.envVar, genInfo.name)) ?? undefined;
|
|
1152
|
-
if (!videoApiKey) {
|
|
1153
|
-
return { ...result, error: `${genInfo.name} API key required. Run 'vibe setup' or set ${genInfo.envVar} in .env` };
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
let imageApiKey;
|
|
1157
|
-
if (regenerateImage) {
|
|
1158
|
-
const imageProvider = options.imageProvider || "openai";
|
|
1159
|
-
const imageKeyMap = {
|
|
1160
|
-
openai: { envVar: "OPENAI_API_KEY", name: "OpenAI" },
|
|
1161
|
-
gemini: { envVar: "GOOGLE_API_KEY", name: "Google" },
|
|
1162
|
-
grok: { envVar: "XAI_API_KEY", name: "xAI" },
|
|
1163
|
-
};
|
|
1164
|
-
const info = imageKeyMap[imageProvider];
|
|
1165
|
-
if (!info) {
|
|
1166
|
-
return { ...result, error: `Invalid imageProvider: ${imageProvider}` };
|
|
1167
|
-
}
|
|
1168
|
-
imageApiKey = (await getApiKey(info.envVar, info.name)) ?? undefined;
|
|
1169
|
-
if (!imageApiKey) {
|
|
1170
|
-
return { ...result, error: `${info.name} API key required. Run 'vibe setup' or set ${info.envVar} in .env` };
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
let elevenlabsApiKey;
|
|
1174
|
-
if (regenerateNarration) {
|
|
1175
|
-
elevenlabsApiKey = (await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs")) ?? undefined;
|
|
1176
|
-
if (!elevenlabsApiKey) {
|
|
1177
|
-
return { ...result, error: "ElevenLabs API key required. Run 'vibe setup' or set ELEVENLABS_API_KEY in .env" };
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
// Track storyboard mutations so we only rewrite when segment durations change.
|
|
1181
|
-
let storyboardMutated = false;
|
|
1182
|
-
// Process each scene
|
|
1183
|
-
for (const sceneNum of options.scenes) {
|
|
1184
|
-
const segment = segments[sceneNum - 1];
|
|
1185
|
-
const narrationPath = resolve(outputDir, `narration-${sceneNum}.mp3`);
|
|
1186
|
-
const imagePath = resolve(outputDir, `scene-${sceneNum}.png`);
|
|
1187
|
-
const videoPath = resolve(outputDir, `scene-${sceneNum}.mp4`);
|
|
1188
|
-
let sceneFailed = false;
|
|
1189
|
-
// Narration regeneration
|
|
1190
|
-
if (regenerateNarration && elevenlabsApiKey) {
|
|
1191
|
-
options.onProgress?.(`Scene ${sceneNum}: regenerating narration...`);
|
|
1192
|
-
const elevenlabs = new ElevenLabsProvider();
|
|
1193
|
-
await elevenlabs.initialize({ apiKey: elevenlabsApiKey });
|
|
1194
|
-
const narrationText = segment.narration || segment.description;
|
|
1195
|
-
const ttsResult = await elevenlabs.textToSpeech(narrationText, {
|
|
1196
|
-
voiceId: options.voice,
|
|
1197
|
-
});
|
|
1198
|
-
if (ttsResult.success && ttsResult.audioBuffer) {
|
|
1199
|
-
await writeFile(narrationPath, ttsResult.audioBuffer);
|
|
1200
|
-
segment.duration = await getAudioDuration(narrationPath);
|
|
1201
|
-
storyboardMutated = true;
|
|
1202
|
-
}
|
|
1203
|
-
else {
|
|
1204
|
-
sceneFailed = true;
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
// Image regeneration (with character-consistency reference when available)
|
|
1208
|
-
if (!sceneFailed && regenerateImage && imageApiKey) {
|
|
1209
|
-
options.onProgress?.(`Scene ${sceneNum}: regenerating image...`);
|
|
1210
|
-
const imageProvider = options.imageProvider || "openai";
|
|
1211
|
-
const characterDesc = segment.characterDescription || segments[0]?.characterDescription;
|
|
1212
|
-
let imagePrompt = segment.visualStyle
|
|
1213
|
-
? `${segment.visuals}. Style: ${segment.visualStyle}`
|
|
1214
|
-
: segment.visuals;
|
|
1215
|
-
if (characterDesc) {
|
|
1216
|
-
imagePrompt = `${imagePrompt}\n\nIMPORTANT - Character appearance must match exactly: ${characterDesc}`;
|
|
1217
|
-
}
|
|
1218
|
-
// Pick a reference image for character consistency: caller-specified,
|
|
1219
|
-
// else auto-detect the first existing scene image that isn't this scene.
|
|
1220
|
-
let referenceImageBuffer;
|
|
1221
|
-
const refSceneNum = options.referenceScene;
|
|
1222
|
-
if (refSceneNum && refSceneNum >= 1 && refSceneNum <= segments.length && refSceneNum !== sceneNum) {
|
|
1223
|
-
const refImagePath = resolve(outputDir, `scene-${refSceneNum}.png`);
|
|
1224
|
-
if (existsSync(refImagePath)) {
|
|
1225
|
-
referenceImageBuffer = await readFile(refImagePath);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
else if (!refSceneNum) {
|
|
1229
|
-
for (let i = 1; i <= segments.length; i++) {
|
|
1230
|
-
if (i !== sceneNum) {
|
|
1231
|
-
const otherImagePath = resolve(outputDir, `scene-${i}.png`);
|
|
1232
|
-
if (existsSync(otherImagePath)) {
|
|
1233
|
-
referenceImageBuffer = await readFile(otherImagePath);
|
|
1234
|
-
break;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
const dalleImageSizes = {
|
|
1240
|
-
"16:9": "1536x1024",
|
|
1241
|
-
"9:16": "1024x1536",
|
|
1242
|
-
"1:1": "1024x1024",
|
|
1243
|
-
};
|
|
1244
|
-
let imageBuffer;
|
|
1245
|
-
let imageUrl;
|
|
1246
|
-
if (imageProvider === "openai") {
|
|
1247
|
-
const openaiImage = new OpenAIImageProvider();
|
|
1248
|
-
await openaiImage.initialize({ apiKey: imageApiKey });
|
|
1249
|
-
const imageResult = await openaiImage.generateImage(imagePrompt, {
|
|
1250
|
-
size: dalleImageSizes[options.aspectRatio || "16:9"] || "1536x1024",
|
|
1251
|
-
quality: "standard",
|
|
1252
|
-
});
|
|
1253
|
-
if (imageResult.success && imageResult.images?.[0]) {
|
|
1254
|
-
const img = imageResult.images[0];
|
|
1255
|
-
if (img.base64)
|
|
1256
|
-
imageBuffer = Buffer.from(img.base64, "base64");
|
|
1257
|
-
else if (img.url)
|
|
1258
|
-
imageUrl = img.url;
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
else if (imageProvider === "gemini") {
|
|
1262
|
-
const gemini = new GeminiProvider();
|
|
1263
|
-
await gemini.initialize({ apiKey: imageApiKey });
|
|
1264
|
-
if (referenceImageBuffer) {
|
|
1265
|
-
// Gemini edit-with-reference path — pulls the character forward
|
|
1266
|
-
// while describing the new action. Simplified visuals pick the
|
|
1267
|
-
// first recognised action verb to reduce prompt bleed.
|
|
1268
|
-
const simplifiedVisuals = segment.visuals.split(/[,.]/).find((part) => part.includes("standing") || part.includes("sitting") || part.includes("walking") ||
|
|
1269
|
-
part.includes("lying") || part.includes("reaching") || part.includes("looking") ||
|
|
1270
|
-
part.includes("working") || part.includes("coding") || part.includes("typing")) || segment.visuals.split(".")[0];
|
|
1271
|
-
const editPrompt = `Generate a new image showing the SAME SINGLE person from the reference image in a new scene.
|
|
1272
|
-
|
|
1273
|
-
REFERENCE: Look at the person in the reference image - their face, hair, build, and overall appearance.
|
|
1274
|
-
|
|
1275
|
-
NEW SCENE: ${simplifiedVisuals}
|
|
1276
|
-
|
|
1277
|
-
CRITICAL RULES:
|
|
1278
|
-
1. Show ONLY ONE person - the exact same individual from the reference image
|
|
1279
|
-
2. The person must have the IDENTICAL face, hair style, and body type
|
|
1280
|
-
3. Do NOT show multiple people or duplicate the character
|
|
1281
|
-
4. Create a single moment in time, one pose, one action
|
|
1282
|
-
5. Match the art style and quality of the reference image
|
|
1283
|
-
|
|
1284
|
-
Generate the single-person scene image now.`;
|
|
1285
|
-
const imageResult = await gemini.editImage([referenceImageBuffer], editPrompt, {
|
|
1286
|
-
aspectRatio: (options.aspectRatio || "16:9"),
|
|
1287
|
-
});
|
|
1288
|
-
if (imageResult.success && imageResult.images?.[0]?.base64) {
|
|
1289
|
-
imageBuffer = Buffer.from(imageResult.images[0].base64, "base64");
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
else {
|
|
1293
|
-
const imageResult = await gemini.generateImage(imagePrompt, {
|
|
1294
|
-
aspectRatio: (options.aspectRatio || "16:9"),
|
|
1295
|
-
});
|
|
1296
|
-
if (imageResult.success && imageResult.images?.[0]?.base64) {
|
|
1297
|
-
imageBuffer = Buffer.from(imageResult.images[0].base64, "base64");
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
else if (imageProvider === "grok") {
|
|
1302
|
-
const grok = new GrokProvider();
|
|
1303
|
-
await grok.initialize({ apiKey: imageApiKey });
|
|
1304
|
-
const imageResult = await grok.generateImage(imagePrompt, {
|
|
1305
|
-
aspectRatio: options.aspectRatio || "16:9",
|
|
1306
|
-
});
|
|
1307
|
-
if (imageResult.success && imageResult.images?.[0]) {
|
|
1308
|
-
const img = imageResult.images[0];
|
|
1309
|
-
if (img.base64)
|
|
1310
|
-
imageBuffer = Buffer.from(img.base64, "base64");
|
|
1311
|
-
else if (img.url)
|
|
1312
|
-
imageUrl = img.url;
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
if (imageBuffer) {
|
|
1316
|
-
await writeFile(imagePath, imageBuffer);
|
|
1317
|
-
}
|
|
1318
|
-
else if (imageUrl) {
|
|
1319
|
-
const response = await fetch(imageUrl);
|
|
1320
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
1321
|
-
await writeFile(imagePath, buffer);
|
|
1322
|
-
}
|
|
1323
|
-
else {
|
|
1324
|
-
sceneFailed = true;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
if (sceneFailed) {
|
|
1328
|
-
result.failedScenes.push(sceneNum);
|
|
1329
|
-
continue;
|
|
1330
|
-
}
|
|
1331
|
-
if (regenerateVideo && videoApiKey) {
|
|
1332
|
-
options.onProgress?.(`Scene ${sceneNum}: regenerating video (${options.generator || "grok"})...`);
|
|
1333
|
-
if (!existsSync(imagePath)) {
|
|
1334
|
-
result.failedScenes.push(sceneNum);
|
|
1335
|
-
continue;
|
|
1336
|
-
}
|
|
1337
|
-
const imageBuffer = await readFile(imagePath);
|
|
1338
|
-
const videoDuration = (segment.duration > 5 ? 10 : 5);
|
|
1339
|
-
const maxRetries = options.retries ?? DEFAULT_VIDEO_RETRIES;
|
|
1340
|
-
if (options.generator === "grok") {
|
|
1341
|
-
// Grok (xAI) — image-to-video with data URI reference
|
|
1342
|
-
const grok = new GrokProvider();
|
|
1343
|
-
await grok.initialize({ apiKey: videoApiKey });
|
|
1344
|
-
const ext = extname(imagePath).toLowerCase().slice(1);
|
|
1345
|
-
const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
|
1346
|
-
const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
|
|
1347
|
-
const grokDuration = Math.min(15, Math.max(1, segment.duration));
|
|
1348
|
-
const taskResult = await generateVideoWithRetryGrok(grok, segment, {
|
|
1349
|
-
duration: grokDuration,
|
|
1350
|
-
aspectRatio: (options.aspectRatio || "16:9"),
|
|
1351
|
-
referenceImage,
|
|
1352
|
-
}, maxRetries);
|
|
1353
|
-
if (taskResult) {
|
|
1354
|
-
try {
|
|
1355
|
-
const waitResult = await grok.waitForCompletion(taskResult.requestId, undefined, 300000);
|
|
1356
|
-
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
1357
|
-
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
1358
|
-
await writeFile(videoPath, buffer);
|
|
1359
|
-
const targetDuration = segment.duration;
|
|
1360
|
-
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
1361
|
-
if (actualVideoDuration < targetDuration - 0.1) {
|
|
1362
|
-
const extendedPath = resolve(outputDir, `scene-${sceneNum}-extended.mp4`);
|
|
1363
|
-
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
1364
|
-
await unlink(videoPath);
|
|
1365
|
-
await rename(extendedPath, videoPath);
|
|
1366
|
-
}
|
|
1367
|
-
result.regeneratedScenes.push(sceneNum);
|
|
1368
|
-
}
|
|
1369
|
-
else {
|
|
1370
|
-
logSceneFailure("Grok", `scene ${sceneNum}`, waitResult);
|
|
1371
|
-
result.failedScenes.push(sceneNum);
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
catch (err) {
|
|
1375
|
-
logSceneFailure("Grok", `scene ${sceneNum}`, err);
|
|
1376
|
-
result.failedScenes.push(sceneNum);
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
else {
|
|
1380
|
-
result.failedScenes.push(sceneNum);
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
else if (options.generator === "veo") {
|
|
1384
|
-
// Veo (Gemini)
|
|
1385
|
-
const veo = new GeminiProvider();
|
|
1386
|
-
await veo.initialize({ apiKey: videoApiKey });
|
|
1387
|
-
const ext = extname(imagePath).toLowerCase().slice(1);
|
|
1388
|
-
const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
|
1389
|
-
const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
|
|
1390
|
-
const veoDuration = (segment.duration > 6 ? 8 : segment.duration > 4 ? 6 : 4);
|
|
1391
|
-
const taskResult = await generateVideoWithRetryVeo(veo, segment, {
|
|
1392
|
-
duration: veoDuration,
|
|
1393
|
-
aspectRatio: (options.aspectRatio || "16:9"),
|
|
1394
|
-
referenceImage,
|
|
1395
|
-
}, maxRetries);
|
|
1396
|
-
if (taskResult) {
|
|
1397
|
-
try {
|
|
1398
|
-
const waitResult = await veo.waitForVideoCompletion(taskResult.operationName, undefined, 300000);
|
|
1399
|
-
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
1400
|
-
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
1401
|
-
await writeFile(videoPath, buffer);
|
|
1402
|
-
const targetDuration = segment.duration;
|
|
1403
|
-
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
1404
|
-
if (actualVideoDuration < targetDuration - 0.1) {
|
|
1405
|
-
const extendedPath = resolve(outputDir, `scene-${sceneNum}-extended.mp4`);
|
|
1406
|
-
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
1407
|
-
await unlink(videoPath);
|
|
1408
|
-
await rename(extendedPath, videoPath);
|
|
1409
|
-
}
|
|
1410
|
-
result.regeneratedScenes.push(sceneNum);
|
|
1411
|
-
}
|
|
1412
|
-
else {
|
|
1413
|
-
logSceneFailure("Veo", `scene ${sceneNum}`, waitResult);
|
|
1414
|
-
result.failedScenes.push(sceneNum);
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
catch (err) {
|
|
1418
|
-
logSceneFailure("Veo", `scene ${sceneNum}`, err);
|
|
1419
|
-
result.failedScenes.push(sceneNum);
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
else {
|
|
1423
|
-
result.failedScenes.push(sceneNum);
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
else if (options.generator === "kling" || !options.generator) {
|
|
1427
|
-
const kling = new KlingProvider();
|
|
1428
|
-
await kling.initialize({ apiKey: videoApiKey });
|
|
1429
|
-
if (!kling.isConfigured()) {
|
|
1430
|
-
result.failedScenes.push(sceneNum);
|
|
1431
|
-
continue;
|
|
1432
|
-
}
|
|
1433
|
-
// Try to use image-to-video if ImgBB key available
|
|
1434
|
-
const imgbbApiKey = await getApiKeyFromConfig("imgbb") || process.env.IMGBB_API_KEY;
|
|
1435
|
-
let imageUrl;
|
|
1436
|
-
if (imgbbApiKey) {
|
|
1437
|
-
const uploadResult = await uploadToImgbb(imageBuffer, imgbbApiKey);
|
|
1438
|
-
if (uploadResult.success && uploadResult.url) {
|
|
1439
|
-
imageUrl = uploadResult.url;
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
const taskResult = await generateVideoWithRetryKling(kling, segment, {
|
|
1443
|
-
duration: videoDuration,
|
|
1444
|
-
aspectRatio: (options.aspectRatio || "16:9"),
|
|
1445
|
-
referenceImage: imageUrl,
|
|
1446
|
-
}, maxRetries);
|
|
1447
|
-
if (taskResult) {
|
|
1448
|
-
try {
|
|
1449
|
-
const waitResult = await kling.waitForCompletion(taskResult.taskId, taskResult.type, undefined, 600000);
|
|
1450
|
-
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
1451
|
-
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
1452
|
-
await writeFile(videoPath, buffer);
|
|
1453
|
-
// Extend via extendVideoToTarget so Kling's own extend API can
|
|
1454
|
-
// be used for large gaps (ratio > 1.4) instead of freeze frames.
|
|
1455
|
-
await extendVideoToTarget(videoPath, segment.duration, outputDir, `Scene ${sceneNum}`, {
|
|
1456
|
-
kling,
|
|
1457
|
-
videoId: waitResult.videoId,
|
|
1458
|
-
onProgress: options.onProgress,
|
|
1459
|
-
});
|
|
1460
|
-
result.regeneratedScenes.push(sceneNum);
|
|
1461
|
-
}
|
|
1462
|
-
else {
|
|
1463
|
-
logSceneFailure("Kling", `scene ${sceneNum}`, waitResult);
|
|
1464
|
-
result.failedScenes.push(sceneNum);
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
catch (err) {
|
|
1468
|
-
logSceneFailure("Kling", `scene ${sceneNum}`, err);
|
|
1469
|
-
result.failedScenes.push(sceneNum);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
else {
|
|
1473
|
-
result.failedScenes.push(sceneNum);
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
else {
|
|
1477
|
-
// Runway
|
|
1478
|
-
const runway = new RunwayProvider();
|
|
1479
|
-
await runway.initialize({ apiKey: videoApiKey });
|
|
1480
|
-
const ext = extname(imagePath).toLowerCase().slice(1);
|
|
1481
|
-
const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
|
1482
|
-
const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
|
|
1483
|
-
const aspectRatio = options.aspectRatio === "1:1" ? "16:9" : (options.aspectRatio || "16:9");
|
|
1484
|
-
const taskResult = await generateVideoWithRetryRunway(runway, segment, referenceImage, { duration: videoDuration, aspectRatio }, maxRetries);
|
|
1485
|
-
if (taskResult) {
|
|
1486
|
-
try {
|
|
1487
|
-
const waitResult = await runway.waitForCompletion(taskResult.taskId, undefined, 300000);
|
|
1488
|
-
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
1489
|
-
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
1490
|
-
await writeFile(videoPath, buffer);
|
|
1491
|
-
// Extend video to match narration duration if needed
|
|
1492
|
-
const targetDuration = segment.duration;
|
|
1493
|
-
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
1494
|
-
if (actualVideoDuration < targetDuration - 0.1) {
|
|
1495
|
-
const extendedPath = resolve(outputDir, `scene-${sceneNum}-extended.mp4`);
|
|
1496
|
-
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
1497
|
-
await unlink(videoPath);
|
|
1498
|
-
await rename(extendedPath, videoPath);
|
|
1499
|
-
}
|
|
1500
|
-
result.regeneratedScenes.push(sceneNum);
|
|
1501
|
-
}
|
|
1502
|
-
else {
|
|
1503
|
-
logSceneFailure("Runway", `scene ${sceneNum}`, waitResult);
|
|
1504
|
-
result.failedScenes.push(sceneNum);
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
catch (err) {
|
|
1508
|
-
logSceneFailure("Runway", `scene ${sceneNum}`, err);
|
|
1509
|
-
result.failedScenes.push(sceneNum);
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
else {
|
|
1513
|
-
result.failedScenes.push(sceneNum);
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
else if (!sceneFailed) {
|
|
1518
|
-
// narration-only / image-only / both: no video pass, so record success here
|
|
1519
|
-
result.regeneratedScenes.push(sceneNum);
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
// Persist storyboard if narration regeneration changed segment durations.
|
|
1523
|
-
// Preserve the on-disk format (yaml vs json) so no silent downgrades.
|
|
1524
|
-
if (storyboardMutated) {
|
|
1525
|
-
let currentTime = 0;
|
|
1526
|
-
for (const segment of segments) {
|
|
1527
|
-
segment.startTime = currentTime;
|
|
1528
|
-
currentTime += segment.duration;
|
|
1529
|
-
}
|
|
1530
|
-
const serialized = storyboardPath.endsWith(".yaml")
|
|
1531
|
-
? yamlStringify({ scenes: segments }, { indent: 2 })
|
|
1532
|
-
: JSON.stringify(segments, null, 2);
|
|
1533
|
-
await writeFile(storyboardPath, serialized, "utf-8");
|
|
1534
|
-
}
|
|
1535
|
-
result.success = result.failedScenes.length === 0;
|
|
1536
|
-
return result;
|
|
1537
|
-
}
|
|
1538
|
-
catch (error) {
|
|
1539
|
-
return {
|
|
1540
|
-
...result,
|
|
1541
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1542
|
-
};
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
/* CLI command registration moved to ai-script-pipeline-cli.ts */
|
|
1546
|
-
//# sourceMappingURL=ai-script-pipeline.js.map
|