@vibeframe/cli 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-lint.log +21 -0
- package/.turbo/turbo-test.log +689 -0
- package/dist/agent/adapters/claude.d.ts +15 -0
- package/dist/agent/adapters/claude.d.ts.map +1 -0
- package/dist/agent/adapters/claude.js +119 -0
- package/dist/agent/adapters/claude.js.map +1 -0
- package/dist/agent/adapters/gemini.d.ts +15 -0
- package/dist/agent/adapters/gemini.d.ts.map +1 -0
- package/dist/agent/adapters/gemini.js +132 -0
- package/dist/agent/adapters/gemini.js.map +1 -0
- package/dist/agent/adapters/index.d.ts +27 -0
- package/dist/agent/adapters/index.d.ts.map +1 -0
- package/dist/agent/adapters/index.js +38 -0
- package/dist/agent/adapters/index.js.map +1 -0
- package/dist/agent/adapters/ollama.d.ts +20 -0
- package/dist/agent/adapters/ollama.d.ts.map +1 -0
- package/dist/agent/adapters/ollama.js +186 -0
- package/dist/agent/adapters/ollama.js.map +1 -0
- package/dist/agent/adapters/openai.d.ts +15 -0
- package/dist/agent/adapters/openai.d.ts.map +1 -0
- package/dist/agent/adapters/openai.js +92 -0
- package/dist/agent/adapters/openai.js.map +1 -0
- package/dist/agent/adapters/xai.d.ts +15 -0
- package/dist/agent/adapters/xai.d.ts.map +1 -0
- package/dist/agent/adapters/xai.js +95 -0
- package/dist/agent/adapters/xai.js.map +1 -0
- package/dist/agent/index.d.ts +69 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +180 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/memory/index.d.ts +70 -0
- package/dist/agent/memory/index.d.ts.map +1 -0
- package/dist/agent/memory/index.js +132 -0
- package/dist/agent/memory/index.js.map +1 -0
- package/dist/agent/prompts/system.d.ts +6 -0
- package/dist/agent/prompts/system.d.ts.map +1 -0
- package/dist/agent/prompts/system.js +103 -0
- package/dist/agent/prompts/system.js.map +1 -0
- package/dist/agent/tools/ai-editing.d.ts +15 -0
- package/dist/agent/tools/ai-editing.d.ts.map +1 -0
- package/dist/agent/tools/ai-editing.js +763 -0
- package/dist/agent/tools/ai-editing.js.map +1 -0
- package/dist/agent/tools/ai-generation.d.ts +13 -0
- package/dist/agent/tools/ai-generation.d.ts.map +1 -0
- package/dist/agent/tools/ai-generation.js +973 -0
- package/dist/agent/tools/ai-generation.js.map +1 -0
- package/dist/agent/tools/ai-pipeline.d.ts +14 -0
- package/dist/agent/tools/ai-pipeline.d.ts.map +1 -0
- package/dist/agent/tools/ai-pipeline.js +961 -0
- package/dist/agent/tools/ai-pipeline.js.map +1 -0
- package/dist/agent/tools/ai.d.ts +13 -0
- package/dist/agent/tools/ai.d.ts.map +1 -0
- package/dist/agent/tools/ai.js +19 -0
- package/dist/agent/tools/ai.js.map +1 -0
- package/dist/agent/tools/batch.d.ts +6 -0
- package/dist/agent/tools/batch.d.ts.map +1 -0
- package/dist/agent/tools/batch.js +383 -0
- package/dist/agent/tools/batch.js.map +1 -0
- package/dist/agent/tools/e2e.test.d.ts +26 -0
- package/dist/agent/tools/e2e.test.d.ts.map +1 -0
- package/dist/agent/tools/e2e.test.js +397 -0
- package/dist/agent/tools/e2e.test.js.map +1 -0
- package/dist/agent/tools/export.d.ts +6 -0
- package/dist/agent/tools/export.d.ts.map +1 -0
- package/dist/agent/tools/export.js +171 -0
- package/dist/agent/tools/export.js.map +1 -0
- package/dist/agent/tools/filesystem.d.ts +6 -0
- package/dist/agent/tools/filesystem.d.ts.map +1 -0
- package/dist/agent/tools/filesystem.js +212 -0
- package/dist/agent/tools/filesystem.js.map +1 -0
- package/dist/agent/tools/index.d.ts +65 -0
- package/dist/agent/tools/index.d.ts.map +1 -0
- package/dist/agent/tools/index.js +120 -0
- package/dist/agent/tools/index.js.map +1 -0
- package/dist/agent/tools/integration.test.d.ts +11 -0
- package/dist/agent/tools/integration.test.d.ts.map +1 -0
- package/dist/agent/tools/integration.test.js +659 -0
- package/dist/agent/tools/integration.test.js.map +1 -0
- package/dist/agent/tools/media.d.ts +6 -0
- package/dist/agent/tools/media.d.ts.map +1 -0
- package/dist/agent/tools/media.js +616 -0
- package/dist/agent/tools/media.js.map +1 -0
- package/dist/agent/tools/project.d.ts +6 -0
- package/dist/agent/tools/project.d.ts.map +1 -0
- package/dist/agent/tools/project.js +284 -0
- package/dist/agent/tools/project.js.map +1 -0
- package/dist/agent/tools/timeline.d.ts +6 -0
- package/dist/agent/tools/timeline.d.ts.map +1 -0
- package/dist/agent/tools/timeline.js +873 -0
- package/dist/agent/tools/timeline.js.map +1 -0
- package/dist/agent/types.d.ts +59 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +5 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/commands/agent.d.ts +21 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +290 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/ai-analyze.d.ts +106 -0
- package/dist/commands/ai-analyze.d.ts.map +1 -0
- package/dist/commands/ai-analyze.js +327 -0
- package/dist/commands/ai-analyze.js.map +1 -0
- package/dist/commands/ai-animated-caption.d.ts +64 -0
- package/dist/commands/ai-animated-caption.d.ts.map +1 -0
- package/dist/commands/ai-animated-caption.js +272 -0
- package/dist/commands/ai-animated-caption.js.map +1 -0
- package/dist/commands/ai-audio.d.ts +20 -0
- package/dist/commands/ai-audio.d.ts.map +1 -0
- package/dist/commands/ai-audio.js +808 -0
- package/dist/commands/ai-audio.js.map +1 -0
- package/dist/commands/ai-broll.d.ts +15 -0
- package/dist/commands/ai-broll.d.ts.map +1 -0
- package/dist/commands/ai-broll.js +406 -0
- package/dist/commands/ai-broll.js.map +1 -0
- package/dist/commands/ai-edit-cli.d.ts +14 -0
- package/dist/commands/ai-edit-cli.d.ts.map +1 -0
- package/dist/commands/ai-edit-cli.js +579 -0
- package/dist/commands/ai-edit-cli.js.map +1 -0
- package/dist/commands/ai-edit.d.ts +398 -0
- package/dist/commands/ai-edit.d.ts.map +1 -0
- package/dist/commands/ai-edit.js +1019 -0
- package/dist/commands/ai-edit.js.map +1 -0
- package/dist/commands/ai-fill-gaps.d.ts +14 -0
- package/dist/commands/ai-fill-gaps.d.ts.map +1 -0
- package/dist/commands/ai-fill-gaps.js +451 -0
- package/dist/commands/ai-fill-gaps.js.map +1 -0
- package/dist/commands/ai-helpers.d.ts +20 -0
- package/dist/commands/ai-helpers.d.ts.map +1 -0
- package/dist/commands/ai-helpers.js +59 -0
- package/dist/commands/ai-helpers.js.map +1 -0
- package/dist/commands/ai-highlights.d.ts +127 -0
- package/dist/commands/ai-highlights.d.ts.map +1 -0
- package/dist/commands/ai-highlights.js +1026 -0
- package/dist/commands/ai-highlights.js.map +1 -0
- package/dist/commands/ai-image.d.ts +34 -0
- package/dist/commands/ai-image.d.ts.map +1 -0
- package/dist/commands/ai-image.js +653 -0
- package/dist/commands/ai-image.js.map +1 -0
- package/dist/commands/ai-motion.d.ts +50 -0
- package/dist/commands/ai-motion.d.ts.map +1 -0
- package/dist/commands/ai-motion.js +271 -0
- package/dist/commands/ai-motion.js.map +1 -0
- package/dist/commands/ai-narrate.d.ts +66 -0
- package/dist/commands/ai-narrate.d.ts.map +1 -0
- package/dist/commands/ai-narrate.js +329 -0
- package/dist/commands/ai-narrate.js.map +1 -0
- package/dist/commands/ai-review.d.ts +57 -0
- package/dist/commands/ai-review.d.ts.map +1 -0
- package/dist/commands/ai-review.js +251 -0
- package/dist/commands/ai-review.js.map +1 -0
- package/dist/commands/ai-script-pipeline-cli.d.ts +9 -0
- package/dist/commands/ai-script-pipeline-cli.d.ts.map +1 -0
- package/dist/commands/ai-script-pipeline-cli.js +1494 -0
- package/dist/commands/ai-script-pipeline-cli.js.map +1 -0
- package/dist/commands/ai-script-pipeline.d.ts +259 -0
- package/dist/commands/ai-script-pipeline.d.ts.map +1 -0
- package/dist/commands/ai-script-pipeline.js +1027 -0
- package/dist/commands/ai-script-pipeline.js.map +1 -0
- package/dist/commands/ai-suggest-edit.d.ts +14 -0
- package/dist/commands/ai-suggest-edit.d.ts.map +1 -0
- package/dist/commands/ai-suggest-edit.js +220 -0
- package/dist/commands/ai-suggest-edit.js.map +1 -0
- package/dist/commands/ai-video-fx.d.ts +14 -0
- package/dist/commands/ai-video-fx.d.ts.map +1 -0
- package/dist/commands/ai-video-fx.js +395 -0
- package/dist/commands/ai-video-fx.js.map +1 -0
- package/dist/commands/ai-video.d.ts +15 -0
- package/dist/commands/ai-video.d.ts.map +1 -0
- package/dist/commands/ai-video.js +785 -0
- package/dist/commands/ai-video.js.map +1 -0
- package/dist/commands/ai-viral.d.ts +15 -0
- package/dist/commands/ai-viral.d.ts.map +1 -0
- package/dist/commands/ai-viral.js +519 -0
- package/dist/commands/ai-viral.js.map +1 -0
- package/dist/commands/ai-visual-fx.d.ts +14 -0
- package/dist/commands/ai-visual-fx.d.ts.map +1 -0
- package/dist/commands/ai-visual-fx.js +505 -0
- package/dist/commands/ai-visual-fx.js.map +1 -0
- package/dist/commands/ai.d.ts +38 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +225 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/ai.test.d.ts +2 -0
- package/dist/commands/ai.test.d.ts.map +1 -0
- package/dist/commands/ai.test.js +554 -0
- package/dist/commands/ai.test.js.map +1 -0
- package/dist/commands/analyze.d.ts +16 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +247 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/audio.d.ts +18 -0
- package/dist/commands/audio.d.ts.map +1 -0
- package/dist/commands/audio.js +539 -0
- package/dist/commands/audio.js.map +1 -0
- package/dist/commands/batch.d.ts +3 -0
- package/dist/commands/batch.d.ts.map +1 -0
- package/dist/commands/batch.js +366 -0
- package/dist/commands/batch.js.map +1 -0
- package/dist/commands/batch.test.d.ts +2 -0
- package/dist/commands/batch.test.d.ts.map +1 -0
- package/dist/commands/batch.test.js +203 -0
- package/dist/commands/batch.test.js.map +1 -0
- package/dist/commands/detect.d.ts +3 -0
- package/dist/commands/detect.d.ts.map +1 -0
- package/dist/commands/detect.js +273 -0
- package/dist/commands/detect.js.map +1 -0
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +191 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/edit-cmd.d.ts +26 -0
- package/dist/commands/edit-cmd.d.ts.map +1 -0
- package/dist/commands/edit-cmd.js +870 -0
- package/dist/commands/edit-cmd.js.map +1 -0
- package/dist/commands/export.d.ts +39 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +730 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/generate.d.ts +25 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +1885 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/media.d.ts +3 -0
- package/dist/commands/media.d.ts.map +1 -0
- package/dist/commands/media.js +165 -0
- package/dist/commands/media.js.map +1 -0
- package/dist/commands/output.d.ts +45 -0
- package/dist/commands/output.d.ts.map +1 -0
- package/dist/commands/output.js +122 -0
- package/dist/commands/output.js.map +1 -0
- package/dist/commands/pipeline.d.ts +19 -0
- package/dist/commands/pipeline.d.ts.map +1 -0
- package/dist/commands/pipeline.js +345 -0
- package/dist/commands/pipeline.js.map +1 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +139 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/project.test.d.ts +2 -0
- package/dist/commands/project.test.d.ts.map +1 -0
- package/dist/commands/project.test.js +105 -0
- package/dist/commands/project.test.js.map +1 -0
- package/dist/commands/sanitize.d.ts +21 -0
- package/dist/commands/sanitize.d.ts.map +1 -0
- package/dist/commands/sanitize.js +56 -0
- package/dist/commands/sanitize.js.map +1 -0
- package/dist/commands/schema.d.ts +11 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +101 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/setup.d.ts +6 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +440 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/timeline.d.ts +3 -0
- package/dist/commands/timeline.d.ts.map +1 -0
- package/dist/commands/timeline.js +469 -0
- package/dist/commands/timeline.js.map +1 -0
- package/dist/commands/timeline.test.d.ts +2 -0
- package/dist/commands/timeline.test.d.ts.map +1 -0
- package/dist/commands/timeline.test.js +320 -0
- package/dist/commands/timeline.test.js.map +1 -0
- package/dist/commands/validate.d.ts +32 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +63 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/config/config.test.d.ts +2 -0
- package/dist/config/config.test.d.ts.map +1 -0
- package/dist/config/config.test.js +164 -0
- package/dist/config/config.test.js.map +1 -0
- package/dist/config/index.d.ts +35 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +101 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +43 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +42 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +2 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/project.d.ts +84 -0
- package/dist/engine/project.d.ts.map +1 -0
- package/dist/engine/project.js +355 -0
- package/dist/engine/project.js.map +1 -0
- package/dist/engine/project.test.d.ts +2 -0
- package/dist/engine/project.test.d.ts.map +1 -0
- package/dist/engine/project.test.js +599 -0
- package/dist/engine/project.test.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +131 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/api-key.d.ts +36 -0
- package/dist/utils/api-key.d.ts.map +1 -0
- package/dist/utils/api-key.js +211 -0
- package/dist/utils/api-key.js.map +1 -0
- package/dist/utils/api-key.test.d.ts +2 -0
- package/dist/utils/api-key.test.d.ts.map +1 -0
- package/dist/utils/api-key.test.js +35 -0
- package/dist/utils/api-key.test.js.map +1 -0
- package/dist/utils/audio.d.ts +23 -0
- package/dist/utils/audio.d.ts.map +1 -0
- package/dist/utils/audio.js +79 -0
- package/dist/utils/audio.js.map +1 -0
- package/dist/utils/exec-safe.d.ts +22 -0
- package/dist/utils/exec-safe.d.ts.map +1 -0
- package/dist/utils/exec-safe.js +62 -0
- package/dist/utils/exec-safe.js.map +1 -0
- package/dist/utils/first-run.d.ts +13 -0
- package/dist/utils/first-run.d.ts.map +1 -0
- package/dist/utils/first-run.js +48 -0
- package/dist/utils/first-run.js.map +1 -0
- package/dist/utils/provider-resolver.d.ts +15 -0
- package/dist/utils/provider-resolver.d.ts.map +1 -0
- package/dist/utils/provider-resolver.js +42 -0
- package/dist/utils/provider-resolver.js.map +1 -0
- package/dist/utils/remotion.d.ts +210 -0
- package/dist/utils/remotion.d.ts.map +1 -0
- package/dist/utils/remotion.js +731 -0
- package/dist/utils/remotion.js.map +1 -0
- package/dist/utils/subtitle.d.ts +65 -0
- package/dist/utils/subtitle.d.ts.map +1 -0
- package/dist/utils/subtitle.js +135 -0
- package/dist/utils/subtitle.js.map +1 -0
- package/dist/utils/subtitle.test.d.ts +2 -0
- package/dist/utils/subtitle.test.d.ts.map +1 -0
- package/dist/utils/subtitle.test.js +175 -0
- package/dist/utils/subtitle.test.js.map +1 -0
- package/dist/utils/tty.d.ts +45 -0
- package/dist/utils/tty.d.ts.map +1 -0
- package/dist/utils/tty.js +172 -0
- package/dist/utils/tty.js.map +1 -0
- package/package.json +102 -0
- package/src/agent/adapters/claude.ts +143 -0
- package/src/agent/adapters/gemini.ts +159 -0
- package/src/agent/adapters/index.ts +61 -0
- package/src/agent/adapters/ollama.ts +231 -0
- package/src/agent/adapters/openai.ts +116 -0
- package/src/agent/adapters/xai.ts +119 -0
- package/src/agent/index.ts +251 -0
- package/src/agent/memory/index.ts +151 -0
- package/src/agent/prompts/system.ts +106 -0
- package/src/agent/tools/ai-editing.ts +845 -0
- package/src/agent/tools/ai-generation.ts +1073 -0
- package/src/agent/tools/ai-pipeline.ts +1055 -0
- package/src/agent/tools/ai.ts +21 -0
- package/src/agent/tools/batch.ts +429 -0
- package/src/agent/tools/e2e.test.ts +545 -0
- package/src/agent/tools/export.ts +184 -0
- package/src/agent/tools/filesystem.ts +237 -0
- package/src/agent/tools/index.ts +150 -0
- package/src/agent/tools/integration.test.ts +775 -0
- package/src/agent/tools/media.ts +697 -0
- package/src/agent/tools/project.ts +313 -0
- package/src/agent/tools/timeline.ts +951 -0
- package/src/agent/types.ts +68 -0
- package/src/commands/agent.ts +340 -0
- package/src/commands/ai-analyze.ts +429 -0
- package/src/commands/ai-animated-caption.ts +390 -0
- package/src/commands/ai-audio.ts +941 -0
- package/src/commands/ai-broll.ts +490 -0
- package/src/commands/ai-edit-cli.ts +658 -0
- package/src/commands/ai-edit.ts +1542 -0
- package/src/commands/ai-fill-gaps.ts +566 -0
- package/src/commands/ai-helpers.ts +65 -0
- package/src/commands/ai-highlights.ts +1303 -0
- package/src/commands/ai-image.ts +761 -0
- package/src/commands/ai-motion.ts +347 -0
- package/src/commands/ai-narrate.ts +451 -0
- package/src/commands/ai-review.ts +309 -0
- package/src/commands/ai-script-pipeline-cli.ts +1710 -0
- package/src/commands/ai-script-pipeline.ts +1365 -0
- package/src/commands/ai-suggest-edit.ts +264 -0
- package/src/commands/ai-video-fx.ts +445 -0
- package/src/commands/ai-video.ts +915 -0
- package/src/commands/ai-viral.ts +595 -0
- package/src/commands/ai-visual-fx.ts +601 -0
- package/src/commands/ai.test.ts +627 -0
- package/src/commands/ai.ts +307 -0
- package/src/commands/analyze.ts +282 -0
- package/src/commands/audio.ts +644 -0
- package/src/commands/batch.test.ts +279 -0
- package/src/commands/batch.ts +440 -0
- package/src/commands/detect.ts +329 -0
- package/src/commands/doctor.ts +237 -0
- package/src/commands/edit-cmd.ts +1014 -0
- package/src/commands/export.ts +918 -0
- package/src/commands/generate.ts +2146 -0
- package/src/commands/media.ts +177 -0
- package/src/commands/output.ts +142 -0
- package/src/commands/pipeline.ts +398 -0
- package/src/commands/project.test.ts +127 -0
- package/src/commands/project.ts +149 -0
- package/src/commands/sanitize.ts +60 -0
- package/src/commands/schema.ts +130 -0
- package/src/commands/setup.ts +509 -0
- package/src/commands/timeline.test.ts +499 -0
- package/src/commands/timeline.ts +529 -0
- package/src/commands/validate.ts +77 -0
- package/src/config/config.test.ts +197 -0
- package/src/config/index.ts +125 -0
- package/src/config/schema.ts +82 -0
- package/src/engine/index.ts +2 -0
- package/src/engine/project.test.ts +702 -0
- package/src/engine/project.ts +439 -0
- package/src/index.ts +146 -0
- package/src/utils/api-key.test.ts +41 -0
- package/src/utils/api-key.ts +247 -0
- package/src/utils/audio.ts +83 -0
- package/src/utils/exec-safe.ts +75 -0
- package/src/utils/first-run.ts +52 -0
- package/src/utils/provider-resolver.ts +56 -0
- package/src/utils/remotion.ts +951 -0
- package/src/utils/subtitle.test.ts +227 -0
- package/src/utils/subtitle.ts +169 -0
- package/src/utils/tty.ts +196 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,1027 @@
|
|
|
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, extname } from "node:path";
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
21
|
+
import chalk from "chalk";
|
|
22
|
+
import { GeminiProvider, OpenAIProvider, OpenAIImageProvider, ClaudeProvider, ElevenLabsProvider, KlingProvider, RunwayProvider, GrokProvider, } from "@vibeframe/ai-providers";
|
|
23
|
+
import { getApiKey } from "../utils/api-key.js";
|
|
24
|
+
import { getApiKeyFromConfig } from "../config/index.js";
|
|
25
|
+
import { Project } from "../engine/index.js";
|
|
26
|
+
import { getAudioDuration, getVideoDuration, extendVideoNaturally } from "../utils/audio.js";
|
|
27
|
+
import { applyTextOverlays } from "./ai-edit.js";
|
|
28
|
+
import { executeReview } from "./ai-review.js";
|
|
29
|
+
import { execSafe } from "../utils/exec-safe.js";
|
|
30
|
+
import { downloadVideo } from "./ai-helpers.js";
|
|
31
|
+
/** Default retry count for video generation API calls. */
|
|
32
|
+
export const DEFAULT_VIDEO_RETRIES = 2;
|
|
33
|
+
/** Delay between retries in milliseconds. */
|
|
34
|
+
export const RETRY_DELAY_MS = 5000;
|
|
35
|
+
/**
|
|
36
|
+
* Sleep helper
|
|
37
|
+
*/
|
|
38
|
+
export function sleep(ms) {
|
|
39
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Upload image to ImgBB and return the URL
|
|
43
|
+
* Used for Kling v2.5/v2.6 image-to-video which requires URL (not base64)
|
|
44
|
+
*/
|
|
45
|
+
export async function uploadToImgbb(imageBuffer, apiKey) {
|
|
46
|
+
try {
|
|
47
|
+
const base64Image = imageBuffer.toString("base64");
|
|
48
|
+
const formData = new URLSearchParams();
|
|
49
|
+
formData.append("key", apiKey);
|
|
50
|
+
formData.append("image", base64Image);
|
|
51
|
+
const response = await fetch("https://api.imgbb.com/1/upload", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
body: formData,
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
return { success: false, error: `ImgBB API error (${response.status}): ${response.statusText}` };
|
|
57
|
+
}
|
|
58
|
+
const data = (await response.json());
|
|
59
|
+
if (data.success && data.data?.url) {
|
|
60
|
+
return { success: true, url: data.data.url };
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
return { success: false, error: data.error?.message || "Upload failed" };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return { success: false, error: String(err) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Extend a video to target duration using Kling extend API when possible,
|
|
72
|
+
* with fallback to FFmpeg-based extendVideoNaturally.
|
|
73
|
+
*
|
|
74
|
+
* When the extension ratio > 1.4 and a Kling provider + videoId are available,
|
|
75
|
+
* uses the Kling video-extend API for natural continuation instead of freeze frames.
|
|
76
|
+
*/
|
|
77
|
+
export async function extendVideoToTarget(videoPath, targetDuration, outputDir, sceneLabel, options) {
|
|
78
|
+
const actualDuration = await getVideoDuration(videoPath);
|
|
79
|
+
if (actualDuration >= targetDuration - 0.1)
|
|
80
|
+
return;
|
|
81
|
+
const ratio = targetDuration / actualDuration;
|
|
82
|
+
const extendedPath = resolve(outputDir, `${basename(videoPath, ".mp4")}-extended.mp4`);
|
|
83
|
+
// Try Kling extend API for large gaps (ratio > 1.4) where freeze frames look bad
|
|
84
|
+
if (ratio > 1.4 && options?.kling && options?.videoId) {
|
|
85
|
+
try {
|
|
86
|
+
options.onProgress?.(`${sceneLabel}: Extending via Kling API...`);
|
|
87
|
+
const extendResult = await options.kling.extendVideo(options.videoId, {
|
|
88
|
+
duration: "5",
|
|
89
|
+
});
|
|
90
|
+
if (extendResult.status !== "failed" && extendResult.id) {
|
|
91
|
+
const waitResult = await options.kling.waitForExtendCompletion(extendResult.id, (status) => {
|
|
92
|
+
options.onProgress?.(`${sceneLabel}: extend ${status.status}...`);
|
|
93
|
+
}, 600000);
|
|
94
|
+
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
95
|
+
// Download extended video
|
|
96
|
+
const extendedVideoPath = resolve(outputDir, `${basename(videoPath, ".mp4")}-kling-ext.mp4`);
|
|
97
|
+
const buffer = await downloadVideo(waitResult.videoUrl);
|
|
98
|
+
await writeFile(extendedVideoPath, buffer);
|
|
99
|
+
// Concatenate original + extension
|
|
100
|
+
const concatPath = resolve(outputDir, `${basename(videoPath, ".mp4")}-concat.mp4`);
|
|
101
|
+
const listPath = resolve(outputDir, `${basename(videoPath, ".mp4")}-concat.txt`);
|
|
102
|
+
await writeFile(listPath, `file '${videoPath}'\nfile '${extendedVideoPath}'`, "utf-8");
|
|
103
|
+
await execSafe("ffmpeg", ["-y", "-f", "concat", "-safe", "0", "-i", listPath, "-c", "copy", concatPath]);
|
|
104
|
+
// Trim to exact target duration if concatenated video is longer
|
|
105
|
+
const concatDuration = await getVideoDuration(concatPath);
|
|
106
|
+
if (concatDuration > targetDuration + 0.5) {
|
|
107
|
+
await execSafe("ffmpeg", ["-y", "-i", concatPath, "-t", targetDuration.toFixed(2), "-c", "copy", extendedPath]);
|
|
108
|
+
await unlink(concatPath);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
await rename(concatPath, extendedPath);
|
|
112
|
+
}
|
|
113
|
+
// Cleanup temp files
|
|
114
|
+
await unlink(extendedVideoPath).catch(() => { });
|
|
115
|
+
await unlink(listPath).catch(() => { });
|
|
116
|
+
await unlink(videoPath);
|
|
117
|
+
await rename(extendedPath, videoPath);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// If Kling extend failed, fall through to FFmpeg fallback
|
|
122
|
+
options.onProgress?.(`${sceneLabel}: Kling extend failed, using FFmpeg fallback...`);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
options.onProgress?.(`${sceneLabel}: Kling extend error, using FFmpeg fallback...`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// FFmpeg-based fallback (slowdown + frame interpolation + freeze frame)
|
|
129
|
+
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
130
|
+
await unlink(videoPath);
|
|
131
|
+
await rename(extendedPath, videoPath);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Generate video with retry logic for Kling provider
|
|
135
|
+
* Supports image-to-video with URL (v2.5/v2.6 models)
|
|
136
|
+
*/
|
|
137
|
+
export async function generateVideoWithRetryKling(kling, segment, options, maxRetries, onProgress) {
|
|
138
|
+
// Build detailed prompt from storyboard segment
|
|
139
|
+
const prompt = segment.visualStyle
|
|
140
|
+
? `${segment.visuals}. Style: ${segment.visualStyle}`
|
|
141
|
+
: segment.visuals;
|
|
142
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
143
|
+
try {
|
|
144
|
+
const result = await kling.generateVideo(prompt, {
|
|
145
|
+
prompt,
|
|
146
|
+
// Pass reference image (base64 or URL) - KlingProvider handles v1.5 fallback for base64
|
|
147
|
+
referenceImage: options.referenceImage,
|
|
148
|
+
duration: options.duration,
|
|
149
|
+
aspectRatio: options.aspectRatio,
|
|
150
|
+
mode: "std", // Use std mode for faster generation
|
|
151
|
+
});
|
|
152
|
+
if (result.status !== "failed" && result.id) {
|
|
153
|
+
return {
|
|
154
|
+
taskId: result.id,
|
|
155
|
+
type: options.referenceImage ? "image2video" : "text2video",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (attempt < maxRetries) {
|
|
159
|
+
onProgress?.(`⚠ Retry ${attempt + 1}/${maxRetries}...`);
|
|
160
|
+
await sleep(RETRY_DELAY_MS);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
165
|
+
if (attempt < maxRetries) {
|
|
166
|
+
onProgress?.(`⚠ Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
167
|
+
await sleep(RETRY_DELAY_MS);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// Log the final error on last attempt
|
|
171
|
+
console.error(chalk.dim(`\n [Kling error: ${errMsg}]`));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Generate video with retry logic for Runway provider
|
|
179
|
+
*/
|
|
180
|
+
export async function generateVideoWithRetryRunway(runway, segment, referenceImage, options, maxRetries, onProgress) {
|
|
181
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
182
|
+
try {
|
|
183
|
+
const result = await runway.generateVideo(segment.visuals, {
|
|
184
|
+
prompt: segment.visuals,
|
|
185
|
+
referenceImage,
|
|
186
|
+
duration: options.duration,
|
|
187
|
+
aspectRatio: options.aspectRatio,
|
|
188
|
+
});
|
|
189
|
+
if (result.status !== "failed" && result.id) {
|
|
190
|
+
return { taskId: result.id };
|
|
191
|
+
}
|
|
192
|
+
if (attempt < maxRetries) {
|
|
193
|
+
onProgress?.(`⚠ Retry ${attempt + 1}/${maxRetries}...`);
|
|
194
|
+
await sleep(RETRY_DELAY_MS);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
199
|
+
if (attempt < maxRetries) {
|
|
200
|
+
onProgress?.(`⚠ Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
201
|
+
await sleep(RETRY_DELAY_MS);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.error(chalk.dim(`\n [Runway error: ${errMsg}]`));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Generate video with retry logic for Veo (Gemini) provider
|
|
212
|
+
*/
|
|
213
|
+
export async function generateVideoWithRetryVeo(gemini, segment, options, maxRetries, onProgress) {
|
|
214
|
+
const prompt = segment.visualStyle
|
|
215
|
+
? `${segment.visuals}. Style: ${segment.visualStyle}`
|
|
216
|
+
: segment.visuals;
|
|
217
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
218
|
+
try {
|
|
219
|
+
const result = await gemini.generateVideo(prompt, {
|
|
220
|
+
prompt,
|
|
221
|
+
referenceImage: options.referenceImage,
|
|
222
|
+
duration: options.duration,
|
|
223
|
+
aspectRatio: options.aspectRatio,
|
|
224
|
+
model: "veo-3.1-fast-generate-preview",
|
|
225
|
+
});
|
|
226
|
+
if (result.status !== "failed" && result.id) {
|
|
227
|
+
return { operationName: result.id };
|
|
228
|
+
}
|
|
229
|
+
if (attempt < maxRetries) {
|
|
230
|
+
onProgress?.(`⚠ Retry ${attempt + 1}/${maxRetries}...`);
|
|
231
|
+
await sleep(RETRY_DELAY_MS);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
236
|
+
if (attempt < maxRetries) {
|
|
237
|
+
onProgress?.(`⚠ Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
238
|
+
await sleep(RETRY_DELAY_MS);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
console.error(chalk.dim(`\n [Veo error: ${errMsg}]`));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Wait for video completion with retry logic
|
|
249
|
+
*/
|
|
250
|
+
export async function waitForVideoWithRetry(provider, taskId, providerType, maxRetries, onProgress, timeout) {
|
|
251
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
252
|
+
try {
|
|
253
|
+
let result;
|
|
254
|
+
if (providerType === "kling") {
|
|
255
|
+
result = await provider.waitForCompletion(taskId, "image2video", (status) => onProgress?.(status.status || "processing"), timeout || 600000);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
result = await provider.waitForCompletion(taskId, (status) => {
|
|
259
|
+
const progress = status.progress !== undefined ? `${status.progress}%` : status.status;
|
|
260
|
+
onProgress?.(progress || "processing");
|
|
261
|
+
}, timeout || 300000);
|
|
262
|
+
}
|
|
263
|
+
if (result.status === "completed" && result.videoUrl) {
|
|
264
|
+
return { videoUrl: result.videoUrl };
|
|
265
|
+
}
|
|
266
|
+
// If failed, try resubmitting on next attempt
|
|
267
|
+
if (attempt < maxRetries) {
|
|
268
|
+
onProgress?.(`⚠ Failed, will need resubmission...`);
|
|
269
|
+
return null; // Signal need for resubmission
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (attempt < maxRetries) {
|
|
274
|
+
onProgress?.(`⚠ Error waiting, retry ${attempt + 1}/${maxRetries}...`);
|
|
275
|
+
await sleep(RETRY_DELAY_MS);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Execute the full script-to-video pipeline programmatically.
|
|
283
|
+
*
|
|
284
|
+
* Pipeline stages:
|
|
285
|
+
* 1. Generate storyboard with Claude
|
|
286
|
+
* 2. Generate per-scene voiceovers with ElevenLabs TTS
|
|
287
|
+
* 3. Generate scene images (OpenAI/Gemini)
|
|
288
|
+
* 4. Generate scene videos (Kling/Runway) with extension to match narration
|
|
289
|
+
* 4.5. Apply text overlays if present in storyboard
|
|
290
|
+
* 5. Assemble .vibe.json project file
|
|
291
|
+
* 6. Optional AI review and auto-fix (Gemini)
|
|
292
|
+
*
|
|
293
|
+
* @param options - Pipeline configuration
|
|
294
|
+
* @returns Result with paths to all generated assets and project file
|
|
295
|
+
*/
|
|
296
|
+
export async function executeScriptToVideo(options) {
|
|
297
|
+
const outputDir = options.outputDir || "script-video-output";
|
|
298
|
+
try {
|
|
299
|
+
// Get storyboard provider API key
|
|
300
|
+
const storyboardProvider = options.storyboardProvider || "claude";
|
|
301
|
+
let storyboardApiKey;
|
|
302
|
+
if (storyboardProvider === "openai") {
|
|
303
|
+
storyboardApiKey = (await getApiKey("OPENAI_API_KEY", "OpenAI")) ?? undefined;
|
|
304
|
+
if (!storyboardApiKey) {
|
|
305
|
+
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" };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else if (storyboardProvider === "gemini") {
|
|
309
|
+
storyboardApiKey = (await getApiKey("GOOGLE_API_KEY", "Google")) ?? undefined;
|
|
310
|
+
if (!storyboardApiKey) {
|
|
311
|
+
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" };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// Default: Claude
|
|
316
|
+
storyboardApiKey = (await getApiKey("ANTHROPIC_API_KEY", "Anthropic")) ?? undefined;
|
|
317
|
+
if (!storyboardApiKey) {
|
|
318
|
+
return { success: false, outputDir, scenes: 0, error: "Anthropic API key required for storyboard generation. Run 'vibe setup' or set ANTHROPIC_API_KEY in .env" };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Get image provider API key
|
|
322
|
+
let imageApiKey;
|
|
323
|
+
const imageProvider = options.imageProvider || "openai";
|
|
324
|
+
if (imageProvider === "openai" || imageProvider === "dalle") {
|
|
325
|
+
imageApiKey = (await getApiKey("OPENAI_API_KEY", "OpenAI")) ?? undefined;
|
|
326
|
+
if (!imageApiKey) {
|
|
327
|
+
return { success: false, outputDir, scenes: 0, error: "OpenAI API key required for image generation. Run 'vibe setup' or set OPENAI_API_KEY in .env" };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else if (imageProvider === "gemini") {
|
|
331
|
+
imageApiKey = (await getApiKey("GOOGLE_API_KEY", "Google")) ?? undefined;
|
|
332
|
+
if (!imageApiKey) {
|
|
333
|
+
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" };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else if (imageProvider === "grok") {
|
|
337
|
+
imageApiKey = (await getApiKey("XAI_API_KEY", "xAI")) ?? undefined;
|
|
338
|
+
if (!imageApiKey) {
|
|
339
|
+
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" };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
let elevenlabsApiKey;
|
|
343
|
+
if (!options.noVoiceover) {
|
|
344
|
+
elevenlabsApiKey = (await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs")) ?? undefined;
|
|
345
|
+
if (!elevenlabsApiKey) {
|
|
346
|
+
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" };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
let videoApiKey;
|
|
350
|
+
if (!options.imagesOnly) {
|
|
351
|
+
if (options.generator === "kling") {
|
|
352
|
+
videoApiKey = (await getApiKey("KLING_API_KEY", "Kling")) ?? undefined;
|
|
353
|
+
if (!videoApiKey) {
|
|
354
|
+
return { success: false, outputDir, scenes: 0, error: "Kling API key required (or use imagesOnly option). Run 'vibe setup' or set KLING_API_KEY in .env" };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else if (options.generator === "veo") {
|
|
358
|
+
videoApiKey = (await getApiKey("GOOGLE_API_KEY", "Google")) ?? undefined;
|
|
359
|
+
if (!videoApiKey) {
|
|
360
|
+
return { success: false, outputDir, scenes: 0, error: "Google API key required for Veo video generation (or use imagesOnly option). Run 'vibe setup' or set GOOGLE_API_KEY in .env" };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
videoApiKey = (await getApiKey("RUNWAY_API_SECRET", "Runway")) ?? undefined;
|
|
365
|
+
if (!videoApiKey) {
|
|
366
|
+
return { success: false, outputDir, scenes: 0, error: "Runway API key required (or use imagesOnly option). Run 'vibe setup' or set RUNWAY_API_SECRET in .env" };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Create output directory
|
|
371
|
+
const absOutputDir = resolve(process.cwd(), outputDir);
|
|
372
|
+
if (!existsSync(absOutputDir)) {
|
|
373
|
+
await mkdir(absOutputDir, { recursive: true });
|
|
374
|
+
}
|
|
375
|
+
// Step 1: Generate storyboard
|
|
376
|
+
let segments;
|
|
377
|
+
const creativityOpts = { creativity: options.creativity };
|
|
378
|
+
if (storyboardProvider === "openai") {
|
|
379
|
+
const openai = new OpenAIProvider();
|
|
380
|
+
await openai.initialize({ apiKey: storyboardApiKey });
|
|
381
|
+
segments = await openai.analyzeContent(options.script, options.duration, creativityOpts);
|
|
382
|
+
}
|
|
383
|
+
else if (storyboardProvider === "gemini") {
|
|
384
|
+
const gemini = new GeminiProvider();
|
|
385
|
+
await gemini.initialize({ apiKey: storyboardApiKey });
|
|
386
|
+
segments = await gemini.analyzeContent(options.script, options.duration, creativityOpts);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
const claude = new ClaudeProvider();
|
|
390
|
+
await claude.initialize({ apiKey: storyboardApiKey });
|
|
391
|
+
segments = await claude.analyzeContent(options.script, options.duration, creativityOpts);
|
|
392
|
+
}
|
|
393
|
+
if (segments.length === 0) {
|
|
394
|
+
return { success: false, outputDir, scenes: 0, error: "Failed to generate storyboard" };
|
|
395
|
+
}
|
|
396
|
+
// Save storyboard
|
|
397
|
+
const storyboardPath = resolve(absOutputDir, "storyboard.json");
|
|
398
|
+
await writeFile(storyboardPath, JSON.stringify(segments, null, 2), "utf-8");
|
|
399
|
+
const result = {
|
|
400
|
+
success: true,
|
|
401
|
+
outputDir: absOutputDir,
|
|
402
|
+
scenes: segments.length,
|
|
403
|
+
storyboardPath,
|
|
404
|
+
narrations: [],
|
|
405
|
+
narrationEntries: [],
|
|
406
|
+
images: [],
|
|
407
|
+
videos: [],
|
|
408
|
+
failedScenes: [],
|
|
409
|
+
failedNarrations: [],
|
|
410
|
+
};
|
|
411
|
+
// Step 2: Generate per-scene voiceovers with ElevenLabs
|
|
412
|
+
if (!options.noVoiceover && elevenlabsApiKey) {
|
|
413
|
+
const elevenlabs = new ElevenLabsProvider();
|
|
414
|
+
await elevenlabs.initialize({ apiKey: elevenlabsApiKey });
|
|
415
|
+
for (let i = 0; i < segments.length; i++) {
|
|
416
|
+
const segment = segments[i];
|
|
417
|
+
const narrationText = segment.narration || segment.description;
|
|
418
|
+
if (!narrationText) {
|
|
419
|
+
// No narration text for this segment - add placeholder entry
|
|
420
|
+
result.narrationEntries.push({
|
|
421
|
+
path: null,
|
|
422
|
+
duration: segment.duration,
|
|
423
|
+
segmentIndex: i,
|
|
424
|
+
failed: false, // Not failed, just no text
|
|
425
|
+
});
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const ttsResult = await elevenlabs.textToSpeech(narrationText, {
|
|
429
|
+
voiceId: options.voice,
|
|
430
|
+
});
|
|
431
|
+
if (ttsResult.success && ttsResult.audioBuffer) {
|
|
432
|
+
const audioPath = resolve(absOutputDir, `narration-${i + 1}.mp3`);
|
|
433
|
+
await writeFile(audioPath, ttsResult.audioBuffer);
|
|
434
|
+
// Get actual audio duration
|
|
435
|
+
const actualDuration = await getAudioDuration(audioPath);
|
|
436
|
+
segment.duration = actualDuration;
|
|
437
|
+
// Add to both arrays for backwards compatibility
|
|
438
|
+
result.narrations.push(audioPath);
|
|
439
|
+
result.narrationEntries.push({
|
|
440
|
+
path: audioPath,
|
|
441
|
+
duration: actualDuration,
|
|
442
|
+
segmentIndex: i,
|
|
443
|
+
failed: false,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// TTS failed - add placeholder entry with error info
|
|
448
|
+
result.narrationEntries.push({
|
|
449
|
+
path: null,
|
|
450
|
+
duration: segment.duration, // Keep original estimated duration
|
|
451
|
+
segmentIndex: i,
|
|
452
|
+
failed: true,
|
|
453
|
+
error: ttsResult.error || "Unknown TTS error",
|
|
454
|
+
});
|
|
455
|
+
result.failedNarrations.push(i + 1); // 1-indexed for user display
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Recalculate startTime for all segments
|
|
459
|
+
let currentTime = 0;
|
|
460
|
+
for (const segment of segments) {
|
|
461
|
+
segment.startTime = currentTime;
|
|
462
|
+
currentTime += segment.duration;
|
|
463
|
+
}
|
|
464
|
+
// Re-save storyboard with updated durations
|
|
465
|
+
await writeFile(storyboardPath, JSON.stringify(segments, null, 2), "utf-8");
|
|
466
|
+
}
|
|
467
|
+
// Step 3: Generate images
|
|
468
|
+
const dalleImageSizes = {
|
|
469
|
+
"16:9": "1536x1024",
|
|
470
|
+
"9:16": "1024x1536",
|
|
471
|
+
"1:1": "1024x1024",
|
|
472
|
+
};
|
|
473
|
+
let openaiImageInstance;
|
|
474
|
+
let geminiInstance;
|
|
475
|
+
let grokInstance;
|
|
476
|
+
if (imageProvider === "openai" || imageProvider === "dalle") {
|
|
477
|
+
openaiImageInstance = new OpenAIImageProvider();
|
|
478
|
+
await openaiImageInstance.initialize({ apiKey: imageApiKey });
|
|
479
|
+
}
|
|
480
|
+
else if (imageProvider === "gemini") {
|
|
481
|
+
geminiInstance = new GeminiProvider();
|
|
482
|
+
await geminiInstance.initialize({ apiKey: imageApiKey });
|
|
483
|
+
}
|
|
484
|
+
else if (imageProvider === "grok") {
|
|
485
|
+
grokInstance = new GrokProvider();
|
|
486
|
+
await grokInstance.initialize({ apiKey: imageApiKey });
|
|
487
|
+
}
|
|
488
|
+
const imagePaths = [];
|
|
489
|
+
for (let i = 0; i < segments.length; i++) {
|
|
490
|
+
const segment = segments[i];
|
|
491
|
+
const imagePrompt = segment.visualStyle
|
|
492
|
+
? `${segment.visuals}. Style: ${segment.visualStyle}`
|
|
493
|
+
: segment.visuals;
|
|
494
|
+
try {
|
|
495
|
+
let imageBuffer;
|
|
496
|
+
let imageUrl;
|
|
497
|
+
if ((imageProvider === "openai" || imageProvider === "dalle") && openaiImageInstance) {
|
|
498
|
+
const imageResult = await openaiImageInstance.generateImage(imagePrompt, {
|
|
499
|
+
size: dalleImageSizes[options.aspectRatio || "16:9"] || "1536x1024",
|
|
500
|
+
quality: "standard",
|
|
501
|
+
});
|
|
502
|
+
if (imageResult.success && imageResult.images && imageResult.images.length > 0) {
|
|
503
|
+
// GPT Image 1.5 returns base64, DALL-E 3 returns URL
|
|
504
|
+
const img = imageResult.images[0];
|
|
505
|
+
if (img.base64) {
|
|
506
|
+
imageBuffer = Buffer.from(img.base64, "base64");
|
|
507
|
+
}
|
|
508
|
+
else if (img.url) {
|
|
509
|
+
imageUrl = img.url;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// else: imageResult.error is available but not captured
|
|
513
|
+
}
|
|
514
|
+
else if (imageProvider === "gemini" && geminiInstance) {
|
|
515
|
+
const imageResult = await geminiInstance.generateImage(imagePrompt, {
|
|
516
|
+
aspectRatio: (options.aspectRatio || "16:9"),
|
|
517
|
+
});
|
|
518
|
+
if (imageResult.success && imageResult.images?.[0]?.base64) {
|
|
519
|
+
imageBuffer = Buffer.from(imageResult.images[0].base64, "base64");
|
|
520
|
+
}
|
|
521
|
+
// else: imageResult.error is available but not captured
|
|
522
|
+
}
|
|
523
|
+
else if (imageProvider === "grok" && grokInstance) {
|
|
524
|
+
const imageResult = await grokInstance.generateImage(imagePrompt, {
|
|
525
|
+
aspectRatio: options.aspectRatio || "16:9",
|
|
526
|
+
});
|
|
527
|
+
if (imageResult.success && imageResult.images && imageResult.images.length > 0) {
|
|
528
|
+
const img = imageResult.images[0];
|
|
529
|
+
if (img.base64) {
|
|
530
|
+
imageBuffer = Buffer.from(img.base64, "base64");
|
|
531
|
+
}
|
|
532
|
+
else if (img.url) {
|
|
533
|
+
imageUrl = img.url;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const imagePath = resolve(absOutputDir, `scene-${i + 1}.png`);
|
|
538
|
+
if (imageBuffer) {
|
|
539
|
+
await writeFile(imagePath, imageBuffer);
|
|
540
|
+
imagePaths.push(imagePath);
|
|
541
|
+
result.images.push(imagePath);
|
|
542
|
+
}
|
|
543
|
+
else if (imageUrl) {
|
|
544
|
+
const response = await fetch(imageUrl);
|
|
545
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
546
|
+
await writeFile(imagePath, buffer);
|
|
547
|
+
imagePaths.push(imagePath);
|
|
548
|
+
result.images.push(imagePath);
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
// Track failed scene - error details not captured (see provider imageResult.error)
|
|
552
|
+
// The failedScenes array tracks which scenes failed for the caller
|
|
553
|
+
imagePaths.push("");
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
imagePaths.push("");
|
|
558
|
+
}
|
|
559
|
+
// Rate limiting delay
|
|
560
|
+
if (i < segments.length - 1) {
|
|
561
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Step 4: Generate videos (if not images-only)
|
|
565
|
+
const videoPaths = [];
|
|
566
|
+
const maxRetries = options.retries ?? DEFAULT_VIDEO_RETRIES;
|
|
567
|
+
if (!options.imagesOnly && videoApiKey) {
|
|
568
|
+
if (options.generator === "kling") {
|
|
569
|
+
const kling = new KlingProvider();
|
|
570
|
+
await kling.initialize({ apiKey: videoApiKey });
|
|
571
|
+
if (!kling.isConfigured()) {
|
|
572
|
+
return { success: false, outputDir: absOutputDir, scenes: segments.length, error: "Invalid Kling API key format. Use ACCESS_KEY:SECRET_KEY" };
|
|
573
|
+
}
|
|
574
|
+
for (let i = 0; i < segments.length; i++) {
|
|
575
|
+
if (!imagePaths[i]) {
|
|
576
|
+
videoPaths.push("");
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const segment = segments[i];
|
|
580
|
+
const videoDuration = (segment.duration > 5 ? 10 : 5);
|
|
581
|
+
// Using text2video since Kling's image2video requires URL (not base64)
|
|
582
|
+
const taskResult = await generateVideoWithRetryKling(kling, segment, { duration: videoDuration, aspectRatio: (options.aspectRatio || "16:9") }, maxRetries);
|
|
583
|
+
if (taskResult) {
|
|
584
|
+
try {
|
|
585
|
+
const waitResult = await kling.waitForCompletion(taskResult.taskId, taskResult.type, undefined, 600000);
|
|
586
|
+
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
587
|
+
const videoPath = resolve(absOutputDir, `scene-${i + 1}.mp4`);
|
|
588
|
+
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
589
|
+
await writeFile(videoPath, buffer);
|
|
590
|
+
// Extend video to match narration duration if needed
|
|
591
|
+
const targetDuration = segment.duration; // Already updated to narration length
|
|
592
|
+
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
593
|
+
if (actualVideoDuration < targetDuration - 0.1) {
|
|
594
|
+
const extendedPath = resolve(absOutputDir, `scene-${i + 1}-extended.mp4`);
|
|
595
|
+
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
596
|
+
// Replace original with extended version
|
|
597
|
+
await unlink(videoPath);
|
|
598
|
+
await rename(extendedPath, videoPath);
|
|
599
|
+
}
|
|
600
|
+
videoPaths.push(videoPath);
|
|
601
|
+
result.videos.push(videoPath);
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
videoPaths.push("");
|
|
605
|
+
result.failedScenes.push(i + 1);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
videoPaths.push("");
|
|
610
|
+
result.failedScenes.push(i + 1);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
videoPaths.push("");
|
|
615
|
+
result.failedScenes.push(i + 1);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else if (options.generator === "veo") {
|
|
620
|
+
// Veo (Gemini)
|
|
621
|
+
const veo = new GeminiProvider();
|
|
622
|
+
await veo.initialize({ apiKey: videoApiKey });
|
|
623
|
+
for (let i = 0; i < segments.length; i++) {
|
|
624
|
+
if (!imagePaths[i]) {
|
|
625
|
+
videoPaths.push("");
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const segment = segments[i];
|
|
629
|
+
const veoDuration = (segment.duration > 6 ? 8 : segment.duration > 4 ? 6 : 4);
|
|
630
|
+
const taskResult = await generateVideoWithRetryVeo(veo, segment, { duration: veoDuration, aspectRatio: (options.aspectRatio || "16:9") }, maxRetries);
|
|
631
|
+
if (taskResult) {
|
|
632
|
+
try {
|
|
633
|
+
const waitResult = await veo.waitForVideoCompletion(taskResult.operationName, undefined, 300000);
|
|
634
|
+
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
635
|
+
const videoPath = resolve(absOutputDir, `scene-${i + 1}.mp4`);
|
|
636
|
+
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
637
|
+
await writeFile(videoPath, buffer);
|
|
638
|
+
// Extend video to match narration duration if needed
|
|
639
|
+
const targetDuration = segment.duration;
|
|
640
|
+
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
641
|
+
if (actualVideoDuration < targetDuration - 0.1) {
|
|
642
|
+
const extendedPath = resolve(absOutputDir, `scene-${i + 1}-extended.mp4`);
|
|
643
|
+
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
644
|
+
await unlink(videoPath);
|
|
645
|
+
await rename(extendedPath, videoPath);
|
|
646
|
+
}
|
|
647
|
+
videoPaths.push(videoPath);
|
|
648
|
+
result.videos.push(videoPath);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
videoPaths.push("");
|
|
652
|
+
result.failedScenes.push(i + 1);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
videoPaths.push("");
|
|
657
|
+
result.failedScenes.push(i + 1);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
videoPaths.push("");
|
|
662
|
+
result.failedScenes.push(i + 1);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
// Runway
|
|
668
|
+
const runway = new RunwayProvider();
|
|
669
|
+
await runway.initialize({ apiKey: videoApiKey });
|
|
670
|
+
for (let i = 0; i < segments.length; i++) {
|
|
671
|
+
if (!imagePaths[i]) {
|
|
672
|
+
videoPaths.push("");
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const segment = segments[i];
|
|
676
|
+
const imageBuffer = await readFile(imagePaths[i]);
|
|
677
|
+
const ext = extname(imagePaths[i]).toLowerCase().slice(1);
|
|
678
|
+
const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
|
679
|
+
const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
|
|
680
|
+
const videoDuration = (segment.duration > 5 ? 10 : 5);
|
|
681
|
+
const aspectRatio = options.aspectRatio === "1:1" ? "16:9" : (options.aspectRatio || "16:9");
|
|
682
|
+
const taskResult = await generateVideoWithRetryRunway(runway, segment, referenceImage, { duration: videoDuration, aspectRatio }, maxRetries);
|
|
683
|
+
if (taskResult) {
|
|
684
|
+
try {
|
|
685
|
+
const waitResult = await runway.waitForCompletion(taskResult.taskId, undefined, 300000);
|
|
686
|
+
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
687
|
+
const videoPath = resolve(absOutputDir, `scene-${i + 1}.mp4`);
|
|
688
|
+
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
689
|
+
await writeFile(videoPath, buffer);
|
|
690
|
+
// Extend video to match narration duration if needed
|
|
691
|
+
const targetDuration = segment.duration;
|
|
692
|
+
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
693
|
+
if (actualVideoDuration < targetDuration - 0.1) {
|
|
694
|
+
const extendedPath = resolve(absOutputDir, `scene-${i + 1}-extended.mp4`);
|
|
695
|
+
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
696
|
+
await unlink(videoPath);
|
|
697
|
+
await rename(extendedPath, videoPath);
|
|
698
|
+
}
|
|
699
|
+
videoPaths.push(videoPath);
|
|
700
|
+
result.videos.push(videoPath);
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
videoPaths.push("");
|
|
704
|
+
result.failedScenes.push(i + 1);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
videoPaths.push("");
|
|
709
|
+
result.failedScenes.push(i + 1);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
videoPaths.push("");
|
|
714
|
+
result.failedScenes.push(i + 1);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Step 4.5: Apply text overlays (if segments have textOverlays)
|
|
720
|
+
if (!options.noTextOverlay) {
|
|
721
|
+
for (let i = 0; i < segments.length; i++) {
|
|
722
|
+
const segment = segments[i];
|
|
723
|
+
if (segment.textOverlays && segment.textOverlays.length > 0 && videoPaths[i] && videoPaths[i] !== "") {
|
|
724
|
+
try {
|
|
725
|
+
const overlayOutput = videoPaths[i].replace(/(\.[^.]+)$/, "-overlay$1");
|
|
726
|
+
const overlayResult = await applyTextOverlays({
|
|
727
|
+
videoPath: videoPaths[i],
|
|
728
|
+
texts: segment.textOverlays,
|
|
729
|
+
outputPath: overlayOutput,
|
|
730
|
+
style: options.textStyle || "lower-third",
|
|
731
|
+
});
|
|
732
|
+
if (overlayResult.success && overlayResult.outputPath) {
|
|
733
|
+
videoPaths[i] = overlayResult.outputPath;
|
|
734
|
+
}
|
|
735
|
+
// Silent fallback: keep original on failure
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// Silent fallback: keep original video
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// Step 5: Create project file
|
|
744
|
+
const project = new Project("Script-to-Video Output");
|
|
745
|
+
project.setAspectRatio((options.aspectRatio || "16:9"));
|
|
746
|
+
// Clear default tracks
|
|
747
|
+
const defaultTracks = project.getTracks();
|
|
748
|
+
for (const track of defaultTracks) {
|
|
749
|
+
project.removeTrack(track.id);
|
|
750
|
+
}
|
|
751
|
+
const videoTrack = project.addTrack({
|
|
752
|
+
name: "Video",
|
|
753
|
+
type: "video",
|
|
754
|
+
order: 1,
|
|
755
|
+
isMuted: false,
|
|
756
|
+
isLocked: false,
|
|
757
|
+
isVisible: true,
|
|
758
|
+
});
|
|
759
|
+
const audioTrack = project.addTrack({
|
|
760
|
+
name: "Audio",
|
|
761
|
+
type: "audio",
|
|
762
|
+
order: 0,
|
|
763
|
+
isMuted: false,
|
|
764
|
+
isLocked: false,
|
|
765
|
+
isVisible: true,
|
|
766
|
+
});
|
|
767
|
+
// Add narration clips - use narrationEntries for proper segment alignment
|
|
768
|
+
if (result.narrationEntries && result.narrationEntries.length > 0) {
|
|
769
|
+
for (const entry of result.narrationEntries) {
|
|
770
|
+
// Skip failed or missing narrations
|
|
771
|
+
if (entry.failed || !entry.path)
|
|
772
|
+
continue;
|
|
773
|
+
const segment = segments[entry.segmentIndex];
|
|
774
|
+
const narrationDuration = await getAudioDuration(entry.path);
|
|
775
|
+
const audioSource = project.addSource({
|
|
776
|
+
name: `Narration ${entry.segmentIndex + 1}`,
|
|
777
|
+
url: entry.path,
|
|
778
|
+
type: "audio",
|
|
779
|
+
duration: narrationDuration,
|
|
780
|
+
});
|
|
781
|
+
project.addClip({
|
|
782
|
+
sourceId: audioSource.id,
|
|
783
|
+
trackId: audioTrack.id,
|
|
784
|
+
startTime: segment.startTime,
|
|
785
|
+
duration: narrationDuration,
|
|
786
|
+
sourceStartOffset: 0,
|
|
787
|
+
sourceEndOffset: narrationDuration,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
// Add video/image clips
|
|
792
|
+
let currentTime = 0;
|
|
793
|
+
for (let i = 0; i < segments.length; i++) {
|
|
794
|
+
const segment = segments[i];
|
|
795
|
+
const hasVideo = videoPaths[i] && videoPaths[i] !== "";
|
|
796
|
+
const hasImage = imagePaths[i] && imagePaths[i] !== "";
|
|
797
|
+
if (!hasVideo && !hasImage) {
|
|
798
|
+
currentTime += segment.duration;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const assetPath = hasVideo ? videoPaths[i] : imagePaths[i];
|
|
802
|
+
const mediaType = hasVideo ? "video" : "image";
|
|
803
|
+
// Use actual video duration (after extension) instead of segment.duration
|
|
804
|
+
const actualDuration = hasVideo
|
|
805
|
+
? await getVideoDuration(assetPath)
|
|
806
|
+
: segment.duration;
|
|
807
|
+
const source = project.addSource({
|
|
808
|
+
name: `Scene ${i + 1}`,
|
|
809
|
+
url: assetPath,
|
|
810
|
+
type: mediaType,
|
|
811
|
+
duration: actualDuration,
|
|
812
|
+
});
|
|
813
|
+
project.addClip({
|
|
814
|
+
sourceId: source.id,
|
|
815
|
+
trackId: videoTrack.id,
|
|
816
|
+
startTime: currentTime,
|
|
817
|
+
duration: actualDuration,
|
|
818
|
+
sourceStartOffset: 0,
|
|
819
|
+
sourceEndOffset: actualDuration,
|
|
820
|
+
});
|
|
821
|
+
currentTime += actualDuration;
|
|
822
|
+
}
|
|
823
|
+
// Save project file
|
|
824
|
+
const projectPath = resolve(absOutputDir, "project.vibe.json");
|
|
825
|
+
await writeFile(projectPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
|
|
826
|
+
result.projectPath = projectPath;
|
|
827
|
+
result.totalDuration = currentTime;
|
|
828
|
+
// Step 6: AI Review & Auto-fix (optional, --review flag)
|
|
829
|
+
if (options.review) {
|
|
830
|
+
try {
|
|
831
|
+
const storyboardFile = resolve(absOutputDir, "storyboard.json");
|
|
832
|
+
// Export project to temp MP4 for review (use first valid video as proxy)
|
|
833
|
+
const reviewTarget = videoPaths.find((p) => p && p !== "") || imagePaths.find((p) => p && p !== "");
|
|
834
|
+
if (reviewTarget) {
|
|
835
|
+
const reviewResult = await executeReview({
|
|
836
|
+
videoPath: reviewTarget,
|
|
837
|
+
storyboardPath: existsSync(storyboardFile) ? storyboardFile : undefined,
|
|
838
|
+
autoApply: options.reviewAutoApply,
|
|
839
|
+
model: "flash",
|
|
840
|
+
});
|
|
841
|
+
if (reviewResult.success) {
|
|
842
|
+
result.reviewFeedback = reviewResult.feedback;
|
|
843
|
+
result.appliedFixes = reviewResult.appliedFixes;
|
|
844
|
+
result.reviewedVideoPath = reviewResult.outputPath;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
catch {
|
|
849
|
+
// Review is non-critical, continue with result
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
return {
|
|
856
|
+
success: false,
|
|
857
|
+
outputDir,
|
|
858
|
+
scenes: 0,
|
|
859
|
+
error: error instanceof Error ? error.message : String(error),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Regenerate specific scene(s) in an existing script-to-video project.
|
|
865
|
+
*
|
|
866
|
+
* Reads the storyboard.json from the project directory, then regenerates
|
|
867
|
+
* the requested scenes using the specified video/image provider. Supports
|
|
868
|
+
* image-to-video via ImgBB URL upload for Kling.
|
|
869
|
+
*
|
|
870
|
+
* @param options - Scene regeneration configuration
|
|
871
|
+
* @returns Result with lists of regenerated and failed scene numbers
|
|
872
|
+
*/
|
|
873
|
+
export async function executeRegenerateScene(options) {
|
|
874
|
+
const result = {
|
|
875
|
+
success: false,
|
|
876
|
+
regeneratedScenes: [],
|
|
877
|
+
failedScenes: [],
|
|
878
|
+
};
|
|
879
|
+
try {
|
|
880
|
+
const outputDir = resolve(process.cwd(), options.projectDir);
|
|
881
|
+
const storyboardPath = resolve(outputDir, "storyboard.json");
|
|
882
|
+
if (!existsSync(outputDir)) {
|
|
883
|
+
return { ...result, error: `Project directory not found: ${outputDir}` };
|
|
884
|
+
}
|
|
885
|
+
if (!existsSync(storyboardPath)) {
|
|
886
|
+
return { ...result, error: `Storyboard not found: ${storyboardPath}` };
|
|
887
|
+
}
|
|
888
|
+
const storyboardContent = await readFile(storyboardPath, "utf-8");
|
|
889
|
+
const segments = JSON.parse(storyboardContent);
|
|
890
|
+
// Validate scenes
|
|
891
|
+
for (const sceneNum of options.scenes) {
|
|
892
|
+
if (sceneNum < 1 || sceneNum > segments.length) {
|
|
893
|
+
return { ...result, error: `Scene ${sceneNum} does not exist. Storyboard has ${segments.length} scenes.` };
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const regenerateVideo = options.videoOnly || (!options.narrationOnly && !options.imageOnly);
|
|
897
|
+
// Get API keys
|
|
898
|
+
let videoApiKey;
|
|
899
|
+
if (regenerateVideo) {
|
|
900
|
+
if (options.generator === "kling" || !options.generator) {
|
|
901
|
+
videoApiKey = (await getApiKey("KLING_API_KEY", "Kling")) ?? undefined;
|
|
902
|
+
if (!videoApiKey) {
|
|
903
|
+
return { ...result, error: "Kling API key required. Run 'vibe setup' or set KLING_API_KEY in .env" };
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
videoApiKey = (await getApiKey("RUNWAY_API_SECRET", "Runway")) ?? undefined;
|
|
908
|
+
if (!videoApiKey) {
|
|
909
|
+
return { ...result, error: "Runway API key required. Run 'vibe setup' or set RUNWAY_API_SECRET in .env" };
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
// Process each scene
|
|
914
|
+
for (const sceneNum of options.scenes) {
|
|
915
|
+
const segment = segments[sceneNum - 1];
|
|
916
|
+
const imagePath = resolve(outputDir, `scene-${sceneNum}.png`);
|
|
917
|
+
const videoPath = resolve(outputDir, `scene-${sceneNum}.mp4`);
|
|
918
|
+
if (regenerateVideo && videoApiKey) {
|
|
919
|
+
if (!existsSync(imagePath)) {
|
|
920
|
+
result.failedScenes.push(sceneNum);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
const imageBuffer = await readFile(imagePath);
|
|
924
|
+
const videoDuration = (segment.duration > 5 ? 10 : 5);
|
|
925
|
+
const maxRetries = options.retries ?? DEFAULT_VIDEO_RETRIES;
|
|
926
|
+
if (options.generator === "kling" || !options.generator) {
|
|
927
|
+
const kling = new KlingProvider();
|
|
928
|
+
await kling.initialize({ apiKey: videoApiKey });
|
|
929
|
+
if (!kling.isConfigured()) {
|
|
930
|
+
result.failedScenes.push(sceneNum);
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
// Try to use image-to-video if ImgBB key available
|
|
934
|
+
const imgbbApiKey = await getApiKeyFromConfig("imgbb") || process.env.IMGBB_API_KEY;
|
|
935
|
+
let imageUrl;
|
|
936
|
+
if (imgbbApiKey) {
|
|
937
|
+
const uploadResult = await uploadToImgbb(imageBuffer, imgbbApiKey);
|
|
938
|
+
if (uploadResult.success && uploadResult.url) {
|
|
939
|
+
imageUrl = uploadResult.url;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const taskResult = await generateVideoWithRetryKling(kling, segment, {
|
|
943
|
+
duration: videoDuration,
|
|
944
|
+
aspectRatio: (options.aspectRatio || "16:9"),
|
|
945
|
+
referenceImage: imageUrl,
|
|
946
|
+
}, maxRetries);
|
|
947
|
+
if (taskResult) {
|
|
948
|
+
try {
|
|
949
|
+
const waitResult = await kling.waitForCompletion(taskResult.taskId, taskResult.type, undefined, 600000);
|
|
950
|
+
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
951
|
+
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
952
|
+
await writeFile(videoPath, buffer);
|
|
953
|
+
// Extend video to match narration duration if needed
|
|
954
|
+
const targetDuration = segment.duration;
|
|
955
|
+
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
956
|
+
if (actualVideoDuration < targetDuration - 0.1) {
|
|
957
|
+
const extendedPath = resolve(outputDir, `scene-${sceneNum}-extended.mp4`);
|
|
958
|
+
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
959
|
+
await unlink(videoPath);
|
|
960
|
+
await rename(extendedPath, videoPath);
|
|
961
|
+
}
|
|
962
|
+
result.regeneratedScenes.push(sceneNum);
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
result.failedScenes.push(sceneNum);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
catch {
|
|
969
|
+
result.failedScenes.push(sceneNum);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
result.failedScenes.push(sceneNum);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
// Runway
|
|
978
|
+
const runway = new RunwayProvider();
|
|
979
|
+
await runway.initialize({ apiKey: videoApiKey });
|
|
980
|
+
const ext = extname(imagePath).toLowerCase().slice(1);
|
|
981
|
+
const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
|
982
|
+
const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
|
|
983
|
+
const aspectRatio = options.aspectRatio === "1:1" ? "16:9" : (options.aspectRatio || "16:9");
|
|
984
|
+
const taskResult = await generateVideoWithRetryRunway(runway, segment, referenceImage, { duration: videoDuration, aspectRatio }, maxRetries);
|
|
985
|
+
if (taskResult) {
|
|
986
|
+
try {
|
|
987
|
+
const waitResult = await runway.waitForCompletion(taskResult.taskId, undefined, 300000);
|
|
988
|
+
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
989
|
+
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
990
|
+
await writeFile(videoPath, buffer);
|
|
991
|
+
// Extend video to match narration duration if needed
|
|
992
|
+
const targetDuration = segment.duration;
|
|
993
|
+
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
994
|
+
if (actualVideoDuration < targetDuration - 0.1) {
|
|
995
|
+
const extendedPath = resolve(outputDir, `scene-${sceneNum}-extended.mp4`);
|
|
996
|
+
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
997
|
+
await unlink(videoPath);
|
|
998
|
+
await rename(extendedPath, videoPath);
|
|
999
|
+
}
|
|
1000
|
+
result.regeneratedScenes.push(sceneNum);
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
result.failedScenes.push(sceneNum);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
catch {
|
|
1007
|
+
result.failedScenes.push(sceneNum);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
result.failedScenes.push(sceneNum);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
result.success = result.failedScenes.length === 0;
|
|
1017
|
+
return result;
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
return {
|
|
1021
|
+
...result,
|
|
1022
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/* CLI command registration moved to ai-script-pipeline-cli.ts */
|
|
1027
|
+
//# sourceMappingURL=ai-script-pipeline.js.map
|