@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,730 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFile, access, stat } from "node:fs/promises";
|
|
3
|
+
import { resolve, basename } from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import { Project } from "../engine/index.js";
|
|
8
|
+
import { execSafe, ffprobeDuration } from "../utils/exec-safe.js";
|
|
9
|
+
/**
|
|
10
|
+
* Resolve project file path - handles both file paths and directory paths
|
|
11
|
+
* If path is a directory, looks for project.vibe.json inside
|
|
12
|
+
*/
|
|
13
|
+
async function resolveProjectPath(inputPath) {
|
|
14
|
+
const filePath = resolve(process.cwd(), inputPath);
|
|
15
|
+
try {
|
|
16
|
+
const stats = await stat(filePath);
|
|
17
|
+
if (stats.isDirectory()) {
|
|
18
|
+
return resolve(filePath, "project.vibe.json");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Path doesn't exist or other error - let readFile handle it
|
|
23
|
+
}
|
|
24
|
+
return filePath;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get the duration of a media file using ffprobe
|
|
28
|
+
* For images, returns a default duration since they have no inherent time
|
|
29
|
+
*/
|
|
30
|
+
export async function getMediaDuration(filePath, mediaType, defaultImageDuration = 5) {
|
|
31
|
+
if (mediaType === "image") {
|
|
32
|
+
return defaultImageDuration;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return await ffprobeDuration(filePath);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return defaultImageDuration;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if a media file has an audio stream
|
|
43
|
+
*/
|
|
44
|
+
export async function checkHasAudio(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
const { stdout } = await execSafe("ffprobe", [
|
|
47
|
+
"-v", "error", "-select_streams", "a", "-show_entries", "stream=codec_type", "-of", "default=noprint_wrappers=1:nokey=1", filePath,
|
|
48
|
+
]);
|
|
49
|
+
return stdout.trim().length > 0;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Reusable export function for programmatic usage
|
|
57
|
+
*/
|
|
58
|
+
export async function runExport(projectPath, outputPath, options = {}) {
|
|
59
|
+
const { preset = "standard", format = "mp4", overwrite = false, gapFill = "extend" } = options;
|
|
60
|
+
try {
|
|
61
|
+
// Check if FFmpeg is installed
|
|
62
|
+
const ffmpegPath = await findFFmpeg();
|
|
63
|
+
if (!ffmpegPath) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
message: "FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Load project
|
|
70
|
+
const filePath = await resolveProjectPath(projectPath);
|
|
71
|
+
const content = await readFile(filePath, "utf-8");
|
|
72
|
+
const data = JSON.parse(content);
|
|
73
|
+
const project = Project.fromJSON(data);
|
|
74
|
+
const summary = project.getSummary();
|
|
75
|
+
if (summary.clipCount === 0) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
message: "Project has no clips to export",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Determine output path
|
|
82
|
+
const finalOutputPath = resolve(process.cwd(), outputPath);
|
|
83
|
+
// Get preset settings
|
|
84
|
+
const presetSettings = getPresetSettings(preset, summary.aspectRatio);
|
|
85
|
+
// Get clips sorted by start time
|
|
86
|
+
const clips = project.getClips().sort((a, b) => a.startTime - b.startTime);
|
|
87
|
+
const sources = project.getSources();
|
|
88
|
+
// Verify source files exist and check for audio streams
|
|
89
|
+
const sourceAudioMap = new Map();
|
|
90
|
+
for (const clip of clips) {
|
|
91
|
+
const source = sources.find((s) => s.id === clip.sourceId);
|
|
92
|
+
if (source) {
|
|
93
|
+
try {
|
|
94
|
+
await access(source.url);
|
|
95
|
+
// Check if video source has audio
|
|
96
|
+
if (source.type === "video" && !sourceAudioMap.has(source.id)) {
|
|
97
|
+
sourceAudioMap.set(source.id, await checkHasAudio(source.url));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
message: `Source file not found: ${source.url}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Build FFmpeg command
|
|
109
|
+
const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, finalOutputPath, { overwrite, format, gapFill }, sourceAudioMap);
|
|
110
|
+
// Run FFmpeg
|
|
111
|
+
await runFFmpegProcess(ffmpegPath, ffmpegArgs, () => { });
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
message: `Exported: ${outputPath}`,
|
|
115
|
+
outputPath: finalOutputPath,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
message: `Export failed: ${errorMessage}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export const exportCommand = new Command("export")
|
|
127
|
+
.description("Export project to video file")
|
|
128
|
+
.argument("<project>", "Project file path")
|
|
129
|
+
.option("-o, --output <path>", "Output file path")
|
|
130
|
+
.option("-f, --format <format>", "Output format (mp4, webm, mov)", "mp4")
|
|
131
|
+
.option("-p, --preset <preset>", "Quality preset (draft, standard, high, ultra)", "standard")
|
|
132
|
+
.option("-y, --overwrite", "Overwrite output file if exists", false)
|
|
133
|
+
.option("-g, --gap-fill <strategy>", "Gap filling strategy (black, extend)", "extend")
|
|
134
|
+
.action(async (projectPath, options) => {
|
|
135
|
+
const spinner = ora("Checking FFmpeg...").start();
|
|
136
|
+
try {
|
|
137
|
+
// Check if FFmpeg is installed
|
|
138
|
+
const ffmpegPath = await findFFmpeg();
|
|
139
|
+
if (!ffmpegPath) {
|
|
140
|
+
spinner.fail(chalk.red("FFmpeg not found"));
|
|
141
|
+
console.error();
|
|
142
|
+
console.error(chalk.yellow("Please install FFmpeg:"));
|
|
143
|
+
console.error(chalk.dim(" macOS: brew install ffmpeg"));
|
|
144
|
+
console.error(chalk.dim(" Ubuntu: sudo apt install ffmpeg"));
|
|
145
|
+
console.error(chalk.dim(" Windows: winget install ffmpeg"));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
// Load project
|
|
149
|
+
spinner.text = "Loading project...";
|
|
150
|
+
const filePath = await resolveProjectPath(projectPath);
|
|
151
|
+
const content = await readFile(filePath, "utf-8");
|
|
152
|
+
const data = JSON.parse(content);
|
|
153
|
+
const project = Project.fromJSON(data);
|
|
154
|
+
const summary = project.getSummary();
|
|
155
|
+
if (summary.clipCount === 0) {
|
|
156
|
+
spinner.fail(chalk.red("Project has no clips to export"));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
// Determine output path
|
|
160
|
+
const outputPath = options.output
|
|
161
|
+
? resolve(process.cwd(), options.output)
|
|
162
|
+
: resolve(process.cwd(), `${basename(projectPath, ".vibe.json")}.${options.format}`);
|
|
163
|
+
// Get preset settings
|
|
164
|
+
const presetSettings = getPresetSettings(options.preset, summary.aspectRatio);
|
|
165
|
+
// Get clips sorted by start time
|
|
166
|
+
const clips = project.getClips().sort((a, b) => a.startTime - b.startTime);
|
|
167
|
+
const sources = project.getSources();
|
|
168
|
+
// Verify source files exist and check for audio streams
|
|
169
|
+
spinner.text = "Verifying source files...";
|
|
170
|
+
const sourceAudioMap = new Map();
|
|
171
|
+
for (const clip of clips) {
|
|
172
|
+
const source = sources.find((s) => s.id === clip.sourceId);
|
|
173
|
+
if (source) {
|
|
174
|
+
try {
|
|
175
|
+
await access(source.url);
|
|
176
|
+
// Check if video source has audio
|
|
177
|
+
if (source.type === "video" && !sourceAudioMap.has(source.id)) {
|
|
178
|
+
sourceAudioMap.set(source.id, await checkHasAudio(source.url));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
spinner.fail(chalk.red(`Source file not found: ${source.url}`));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Build FFmpeg command
|
|
188
|
+
spinner.text = "Building export command...";
|
|
189
|
+
const gapFillStrategy = (options.gapFill === "black" ? "black" : "extend");
|
|
190
|
+
const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, outputPath, { ...options, gapFill: gapFillStrategy }, sourceAudioMap);
|
|
191
|
+
if (process.env.DEBUG) {
|
|
192
|
+
console.log("\nFFmpeg command:");
|
|
193
|
+
console.log("ffmpeg", ffmpegArgs.join(" "));
|
|
194
|
+
console.log();
|
|
195
|
+
}
|
|
196
|
+
// Run FFmpeg
|
|
197
|
+
spinner.text = "Encoding...";
|
|
198
|
+
await runFFmpegProcess(ffmpegPath, ffmpegArgs, (progress) => {
|
|
199
|
+
spinner.text = `Encoding... ${progress}%`;
|
|
200
|
+
});
|
|
201
|
+
spinner.succeed(chalk.green(`Exported: ${outputPath}`));
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(chalk.dim(" Duration:"), `${summary.duration.toFixed(1)}s`);
|
|
204
|
+
console.log(chalk.dim(" Clips:"), summary.clipCount);
|
|
205
|
+
console.log(chalk.dim(" Format:"), options.format);
|
|
206
|
+
console.log(chalk.dim(" Preset:"), options.preset);
|
|
207
|
+
console.log(chalk.dim(" Resolution:"), presetSettings.resolution);
|
|
208
|
+
console.log();
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
spinner.fail(chalk.red("Export failed"));
|
|
212
|
+
if (error instanceof Error) {
|
|
213
|
+
console.error(chalk.red(error.message));
|
|
214
|
+
if (process.env.DEBUG) {
|
|
215
|
+
console.error(error.stack);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
/**
|
|
222
|
+
* Find FFmpeg executable
|
|
223
|
+
*/
|
|
224
|
+
async function findFFmpeg() {
|
|
225
|
+
try {
|
|
226
|
+
const { stdout } = await execSafe("which", ["ffmpeg"]);
|
|
227
|
+
return stdout.trim().split("\n")[0];
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
try {
|
|
231
|
+
const { stdout } = await execSafe("where", ["ffmpeg"]);
|
|
232
|
+
return stdout.trim().split("\n")[0];
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Detect gaps in timeline between clips
|
|
241
|
+
* Returns array of gaps with start and end times
|
|
242
|
+
*/
|
|
243
|
+
function detectTimelineGaps(clips, totalDuration) {
|
|
244
|
+
if (clips.length === 0)
|
|
245
|
+
return [];
|
|
246
|
+
const gaps = [];
|
|
247
|
+
const sortedClips = [...clips].sort((a, b) => a.startTime - b.startTime);
|
|
248
|
+
// Check for gap at the start (first clip doesn't start at 0)
|
|
249
|
+
if (sortedClips[0].startTime > 0.001) {
|
|
250
|
+
gaps.push({ start: 0, end: sortedClips[0].startTime });
|
|
251
|
+
}
|
|
252
|
+
// Check for gaps between clips
|
|
253
|
+
for (let i = 0; i < sortedClips.length - 1; i++) {
|
|
254
|
+
const clipEnd = sortedClips[i].startTime + sortedClips[i].duration;
|
|
255
|
+
const nextStart = sortedClips[i + 1].startTime;
|
|
256
|
+
// Allow small tolerance for floating point errors
|
|
257
|
+
if (nextStart > clipEnd + 0.001) {
|
|
258
|
+
gaps.push({ start: clipEnd, end: nextStart });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Check for gap at the end if totalDuration is provided
|
|
262
|
+
if (totalDuration !== undefined) {
|
|
263
|
+
const lastClip = sortedClips[sortedClips.length - 1];
|
|
264
|
+
const lastClipEnd = lastClip.startTime + lastClip.duration;
|
|
265
|
+
if (totalDuration > lastClipEnd + 0.001) {
|
|
266
|
+
gaps.push({ start: lastClipEnd, end: totalDuration });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return gaps;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Create gap fill plans by extending adjacent clips
|
|
273
|
+
* Priority:
|
|
274
|
+
* 1. Extend clip AFTER the gap backwards (if sourceStartOffset > 0)
|
|
275
|
+
* 2. Extend clip BEFORE the gap forwards (if source has unused duration)
|
|
276
|
+
* 3. Fallback to black frames
|
|
277
|
+
*/
|
|
278
|
+
function createGapFillPlans(gaps, clips, sources) {
|
|
279
|
+
const sortedClips = [...clips].sort((a, b) => a.startTime - b.startTime);
|
|
280
|
+
return gaps.map((gap) => {
|
|
281
|
+
const fills = [];
|
|
282
|
+
let remainingStart = gap.start;
|
|
283
|
+
let remainingEnd = gap.end;
|
|
284
|
+
// Find clip AFTER the gap (for extending backwards)
|
|
285
|
+
const clipAfter = sortedClips.find((c) => Math.abs(c.startTime - gap.end) < 0.01);
|
|
286
|
+
// Find clip BEFORE the gap (for extending forwards)
|
|
287
|
+
const clipBefore = sortedClips.find((c) => Math.abs((c.startTime + c.duration) - gap.start) < 0.01);
|
|
288
|
+
// Try extending clip after the gap backwards first
|
|
289
|
+
if (clipAfter && clipAfter.sourceStartOffset > 0.01) {
|
|
290
|
+
const source = sources.find((s) => s.id === clipAfter.sourceId);
|
|
291
|
+
if (source && source.type === "video") {
|
|
292
|
+
const availableExtension = clipAfter.sourceStartOffset;
|
|
293
|
+
const extensionDuration = Math.min(availableExtension, remainingEnd - remainingStart);
|
|
294
|
+
if (extensionDuration > 0.01) {
|
|
295
|
+
// Extend from the gap end backwards
|
|
296
|
+
const fillStart = remainingEnd - extensionDuration;
|
|
297
|
+
const sourceStart = clipAfter.sourceStartOffset - extensionDuration;
|
|
298
|
+
const sourceEnd = clipAfter.sourceStartOffset;
|
|
299
|
+
fills.push({
|
|
300
|
+
type: "extend-after",
|
|
301
|
+
sourceId: source.id,
|
|
302
|
+
sourceUrl: source.url,
|
|
303
|
+
start: fillStart,
|
|
304
|
+
end: remainingEnd,
|
|
305
|
+
sourceStart,
|
|
306
|
+
sourceEnd,
|
|
307
|
+
});
|
|
308
|
+
remainingEnd = fillStart;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// If there's still a gap, try extending clip before the gap forwards
|
|
313
|
+
if (remainingEnd - remainingStart > 0.01 && clipBefore) {
|
|
314
|
+
const source = sources.find((s) => s.id === clipBefore.sourceId);
|
|
315
|
+
if (source && source.type === "video") {
|
|
316
|
+
const usedEndInSource = clipBefore.sourceEndOffset;
|
|
317
|
+
const availableExtension = source.duration - usedEndInSource;
|
|
318
|
+
if (availableExtension > 0.01) {
|
|
319
|
+
const extensionDuration = Math.min(availableExtension, remainingEnd - remainingStart);
|
|
320
|
+
if (extensionDuration > 0.01) {
|
|
321
|
+
const sourceStart = usedEndInSource;
|
|
322
|
+
const sourceEnd = usedEndInSource + extensionDuration;
|
|
323
|
+
fills.push({
|
|
324
|
+
type: "extend-before",
|
|
325
|
+
sourceId: source.id,
|
|
326
|
+
sourceUrl: source.url,
|
|
327
|
+
start: remainingStart,
|
|
328
|
+
end: remainingStart + extensionDuration,
|
|
329
|
+
sourceStart,
|
|
330
|
+
sourceEnd,
|
|
331
|
+
});
|
|
332
|
+
remainingStart = remainingStart + extensionDuration;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Fill any remaining gap with black
|
|
338
|
+
if (remainingEnd - remainingStart > 0.01) {
|
|
339
|
+
fills.push({
|
|
340
|
+
type: "black",
|
|
341
|
+
start: remainingStart,
|
|
342
|
+
end: remainingEnd,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return { gap, fills };
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Build FFmpeg arguments for export
|
|
350
|
+
*/
|
|
351
|
+
function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, sourceAudioMap = new Map()) {
|
|
352
|
+
const args = [];
|
|
353
|
+
// Overwrite flag first
|
|
354
|
+
if (options.overwrite) {
|
|
355
|
+
args.push("-y");
|
|
356
|
+
}
|
|
357
|
+
// Add input files
|
|
358
|
+
const sourceMap = new Map();
|
|
359
|
+
let inputIndex = 0;
|
|
360
|
+
for (const clip of clips) {
|
|
361
|
+
const source = sources.find((s) => s.id === clip.sourceId);
|
|
362
|
+
if (source && !sourceMap.has(source.id)) {
|
|
363
|
+
// Add -loop 1 before image inputs to create a continuous video stream
|
|
364
|
+
if (source.type === "image") {
|
|
365
|
+
args.push("-loop", "1");
|
|
366
|
+
}
|
|
367
|
+
args.push("-i", source.url);
|
|
368
|
+
sourceMap.set(source.id, inputIndex);
|
|
369
|
+
inputIndex++;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// Build filter complex
|
|
373
|
+
const filterParts = [];
|
|
374
|
+
// Separate clips by track type for proper timeline-based export
|
|
375
|
+
// Get track info to determine clip types
|
|
376
|
+
const videoClips = clips.filter((clip) => {
|
|
377
|
+
const source = sources.find((s) => s.id === clip.sourceId);
|
|
378
|
+
return source && (source.type === "image" || source.type === "video");
|
|
379
|
+
}).sort((a, b) => a.startTime - b.startTime);
|
|
380
|
+
// Include audio clips from:
|
|
381
|
+
// 1. Explicit audio sources (narration, music)
|
|
382
|
+
// 2. Video sources when there are NO separate audio clips (e.g., highlight reels)
|
|
383
|
+
const explicitAudioClips = clips.filter((clip) => {
|
|
384
|
+
const source = sources.find((s) => s.id === clip.sourceId);
|
|
385
|
+
return source && source.type === "audio";
|
|
386
|
+
}).sort((a, b) => a.startTime - b.startTime);
|
|
387
|
+
// If no explicit audio clips, extract audio from video clips
|
|
388
|
+
const audioClips = explicitAudioClips.length > 0
|
|
389
|
+
? explicitAudioClips
|
|
390
|
+
: clips.filter((clip) => {
|
|
391
|
+
const source = sources.find((s) => s.id === clip.sourceId);
|
|
392
|
+
return source && source.type === "video";
|
|
393
|
+
}).sort((a, b) => a.startTime - b.startTime);
|
|
394
|
+
// Get target resolution for scaling (all clips must match for concat)
|
|
395
|
+
const [targetWidth, targetHeight] = presetSettings.resolution.split("x").map(Number);
|
|
396
|
+
// Detect gaps in video timeline
|
|
397
|
+
// For totalDuration, use the longest audio clip end time if explicit audio exists
|
|
398
|
+
// (audio is usually the reference for timing in b-roll scenarios)
|
|
399
|
+
let totalDuration;
|
|
400
|
+
if (explicitAudioClips.length > 0) {
|
|
401
|
+
const audioEnd = Math.max(...explicitAudioClips.map(c => c.startTime + c.duration));
|
|
402
|
+
totalDuration = audioEnd;
|
|
403
|
+
}
|
|
404
|
+
const videoGaps = detectTimelineGaps(videoClips, totalDuration);
|
|
405
|
+
// Create gap fill plans based on strategy
|
|
406
|
+
const gapFillStrategy = options.gapFill || "extend";
|
|
407
|
+
const gapFillPlans = gapFillStrategy === "extend"
|
|
408
|
+
? createGapFillPlans(videoGaps, videoClips, sources)
|
|
409
|
+
: videoGaps.map((gap) => ({
|
|
410
|
+
gap,
|
|
411
|
+
fills: [{ type: "black", start: gap.start, end: gap.end }],
|
|
412
|
+
}));
|
|
413
|
+
const videoSegments = [];
|
|
414
|
+
// Add video clips as segments
|
|
415
|
+
for (const clip of videoClips) {
|
|
416
|
+
videoSegments.push({ type: 'clip', clip, startTime: clip.startTime });
|
|
417
|
+
}
|
|
418
|
+
// Add gap fills as segments (from gap fill plans)
|
|
419
|
+
for (const plan of gapFillPlans) {
|
|
420
|
+
for (const fill of plan.fills) {
|
|
421
|
+
if (fill.type === "black") {
|
|
422
|
+
videoSegments.push({
|
|
423
|
+
type: 'black',
|
|
424
|
+
startTime: fill.start,
|
|
425
|
+
duration: fill.end - fill.start,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
// extend-before or extend-after
|
|
430
|
+
videoSegments.push({
|
|
431
|
+
type: 'extended',
|
|
432
|
+
sourceId: fill.sourceId,
|
|
433
|
+
sourceUrl: fill.sourceUrl,
|
|
434
|
+
startTime: fill.start,
|
|
435
|
+
duration: fill.end - fill.start,
|
|
436
|
+
sourceStart: fill.sourceStart,
|
|
437
|
+
sourceEnd: fill.sourceEnd,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Sort by start time
|
|
443
|
+
videoSegments.sort((a, b) => a.startTime - b.startTime);
|
|
444
|
+
// Process video segments (clips, extended clips, and black frames)
|
|
445
|
+
const videoStreams = [];
|
|
446
|
+
let videoStreamIdx = 0;
|
|
447
|
+
for (const segment of videoSegments) {
|
|
448
|
+
if (segment.type === 'clip' && segment.clip) {
|
|
449
|
+
const clip = segment.clip;
|
|
450
|
+
const source = sources.find((s) => s.id === clip.sourceId);
|
|
451
|
+
if (!source)
|
|
452
|
+
continue;
|
|
453
|
+
const srcIdx = sourceMap.get(source.id);
|
|
454
|
+
if (srcIdx === undefined)
|
|
455
|
+
continue;
|
|
456
|
+
// Video filter chain - images need different handling than video
|
|
457
|
+
let videoFilter;
|
|
458
|
+
if (source.type === "image") {
|
|
459
|
+
// Images: trim from 0 to clip duration (no source offset since images are looped)
|
|
460
|
+
videoFilter = `[${srcIdx}:v]trim=start=0:end=${clip.duration},setpts=PTS-STARTPTS`;
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
// Video: use source offsets
|
|
464
|
+
const trimStart = clip.sourceStartOffset;
|
|
465
|
+
const trimEnd = clip.sourceStartOffset + clip.duration;
|
|
466
|
+
videoFilter = `[${srcIdx}:v]trim=start=${trimStart}:end=${trimEnd},setpts=PTS-STARTPTS`;
|
|
467
|
+
}
|
|
468
|
+
// Scale to target resolution for concat compatibility (force same size, pad if needed)
|
|
469
|
+
videoFilter += `,scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
|
|
470
|
+
// Apply effects
|
|
471
|
+
for (const effect of clip.effects || []) {
|
|
472
|
+
if (effect.type === "fadeIn") {
|
|
473
|
+
videoFilter += `,fade=t=in:st=0:d=${effect.duration}`;
|
|
474
|
+
}
|
|
475
|
+
else if (effect.type === "fadeOut") {
|
|
476
|
+
const fadeStart = clip.duration - effect.duration;
|
|
477
|
+
videoFilter += `,fade=t=out:st=${fadeStart}:d=${effect.duration}`;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
videoFilter += `[v${videoStreamIdx}]`;
|
|
481
|
+
filterParts.push(videoFilter);
|
|
482
|
+
videoStreams.push(`[v${videoStreamIdx}]`);
|
|
483
|
+
videoStreamIdx++;
|
|
484
|
+
}
|
|
485
|
+
else if (segment.type === 'extended' && segment.sourceId) {
|
|
486
|
+
// Extended segment - use source video to fill gap
|
|
487
|
+
const srcIdx = sourceMap.get(segment.sourceId);
|
|
488
|
+
if (srcIdx === undefined) {
|
|
489
|
+
// Fallback to black if source not found in input map
|
|
490
|
+
const gapFilter = `color=c=black:s=${targetWidth}x${targetHeight}:d=${segment.duration}:r=30,format=yuv420p[v${videoStreamIdx}]`;
|
|
491
|
+
filterParts.push(gapFilter);
|
|
492
|
+
videoStreams.push(`[v${videoStreamIdx}]`);
|
|
493
|
+
videoStreamIdx++;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const videoFilter = `[${srcIdx}:v]trim=start=${segment.sourceStart}:end=${segment.sourceEnd},setpts=PTS-STARTPTS,scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:(ow-iw)/2:(oh-ih)/2,setsar=1[v${videoStreamIdx}]`;
|
|
497
|
+
filterParts.push(videoFilter);
|
|
498
|
+
videoStreams.push(`[v${videoStreamIdx}]`);
|
|
499
|
+
videoStreamIdx++;
|
|
500
|
+
}
|
|
501
|
+
else if (segment.type === 'black') {
|
|
502
|
+
// Generate black frame for the gap duration
|
|
503
|
+
const gapFilter = `color=c=black:s=${targetWidth}x${targetHeight}:d=${segment.duration}:r=30,format=yuv420p[v${videoStreamIdx}]`;
|
|
504
|
+
filterParts.push(gapFilter);
|
|
505
|
+
videoStreams.push(`[v${videoStreamIdx}]`);
|
|
506
|
+
videoStreamIdx++;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Detect gaps in audio timeline (use same totalDuration for consistency)
|
|
510
|
+
const audioGaps = detectTimelineGaps(audioClips, totalDuration);
|
|
511
|
+
const audioSegments = [];
|
|
512
|
+
// Add audio clips as segments
|
|
513
|
+
for (const clip of audioClips) {
|
|
514
|
+
audioSegments.push({ type: 'clip', clip, startTime: clip.startTime });
|
|
515
|
+
}
|
|
516
|
+
// Add gaps as segments
|
|
517
|
+
for (const gap of audioGaps) {
|
|
518
|
+
audioSegments.push({ type: 'gap', gap, startTime: gap.start });
|
|
519
|
+
}
|
|
520
|
+
// Sort by start time
|
|
521
|
+
audioSegments.sort((a, b) => a.startTime - b.startTime);
|
|
522
|
+
// Process audio segments (clips and gaps)
|
|
523
|
+
const audioStreams = [];
|
|
524
|
+
let audioStreamIdx = 0;
|
|
525
|
+
for (const segment of audioSegments) {
|
|
526
|
+
if (segment.type === 'clip' && segment.clip) {
|
|
527
|
+
const clip = segment.clip;
|
|
528
|
+
const source = sources.find((s) => s.id === clip.sourceId);
|
|
529
|
+
if (!source)
|
|
530
|
+
continue;
|
|
531
|
+
const srcIdx = sourceMap.get(source.id);
|
|
532
|
+
if (srcIdx === undefined)
|
|
533
|
+
continue;
|
|
534
|
+
// Check if source has audio (audio sources always have audio, video sources need to be checked)
|
|
535
|
+
const hasAudio = source.type === "audio" || sourceAudioMap.get(source.id) === true;
|
|
536
|
+
let audioFilter;
|
|
537
|
+
if (hasAudio) {
|
|
538
|
+
const audioTrimStart = clip.sourceStartOffset;
|
|
539
|
+
const audioTrimEnd = clip.sourceStartOffset + clip.duration;
|
|
540
|
+
const sourceDuration = source.duration || 0;
|
|
541
|
+
const clipDuration = clip.duration;
|
|
542
|
+
if (source.type === "audio" && sourceDuration > clipDuration && audioTrimStart === 0) {
|
|
543
|
+
// Audio source is longer than clip slot — speed up to fit instead of truncating
|
|
544
|
+
const tempo = sourceDuration / clipDuration;
|
|
545
|
+
if (tempo <= 2.0) {
|
|
546
|
+
// atempo sounds natural up to ~1.3x, acceptable up to 2x
|
|
547
|
+
audioFilter = `[${srcIdx}:a]atempo=${tempo.toFixed(4)},asetpts=PTS-STARTPTS`;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
// Too fast would sound bad — fall back to trim
|
|
551
|
+
audioFilter = `[${srcIdx}:a]atrim=start=${audioTrimStart}:end=${audioTrimEnd},asetpts=PTS-STARTPTS`;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
// Normal trim for video-embedded audio, audio that fits, or offset clips
|
|
556
|
+
audioFilter = `[${srcIdx}:a]atrim=start=${audioTrimStart}:end=${audioTrimEnd},asetpts=PTS-STARTPTS`;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
// Source has no audio - generate silence for the clip duration
|
|
561
|
+
audioFilter = `anullsrc=r=48000:cl=stereo,atrim=0:${clip.duration},asetpts=PTS-STARTPTS`;
|
|
562
|
+
}
|
|
563
|
+
// Apply audio effects
|
|
564
|
+
for (const effect of clip.effects || []) {
|
|
565
|
+
if (effect.type === "fadeIn") {
|
|
566
|
+
audioFilter += `,afade=t=in:st=0:d=${effect.duration}`;
|
|
567
|
+
}
|
|
568
|
+
else if (effect.type === "fadeOut") {
|
|
569
|
+
const fadeStart = clip.duration - effect.duration;
|
|
570
|
+
audioFilter += `,afade=t=out:st=${fadeStart}:d=${effect.duration}`;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
audioFilter += `[a${audioStreamIdx}]`;
|
|
574
|
+
filterParts.push(audioFilter);
|
|
575
|
+
audioStreams.push(`[a${audioStreamIdx}]`);
|
|
576
|
+
audioStreamIdx++;
|
|
577
|
+
}
|
|
578
|
+
else if (segment.type === 'gap' && segment.gap) {
|
|
579
|
+
// Generate silence for the gap duration
|
|
580
|
+
const gapDuration = segment.gap.end - segment.gap.start;
|
|
581
|
+
const audioGapFilter = `anullsrc=r=48000:cl=stereo,atrim=0:${gapDuration},asetpts=PTS-STARTPTS[a${audioStreamIdx}]`;
|
|
582
|
+
filterParts.push(audioGapFilter);
|
|
583
|
+
audioStreams.push(`[a${audioStreamIdx}]`);
|
|
584
|
+
audioStreamIdx++;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// Concatenate video clips
|
|
588
|
+
if (videoStreams.length > 1) {
|
|
589
|
+
filterParts.push(`${videoStreams.join("")}concat=n=${videoStreams.length}:v=1:a=0[outv]`);
|
|
590
|
+
}
|
|
591
|
+
else if (videoStreams.length === 1) {
|
|
592
|
+
// Single video clip - just copy
|
|
593
|
+
filterParts.push(`${videoStreams[0]}copy[outv]`);
|
|
594
|
+
}
|
|
595
|
+
// Concatenate or mix audio clips
|
|
596
|
+
if (audioStreams.length > 1) {
|
|
597
|
+
filterParts.push(`${audioStreams.join("")}concat=n=${audioStreams.length}:v=0:a=1[outa]`);
|
|
598
|
+
}
|
|
599
|
+
else if (audioStreams.length === 1) {
|
|
600
|
+
// Single audio clip - just copy
|
|
601
|
+
filterParts.push(`${audioStreams[0]}acopy[outa]`);
|
|
602
|
+
}
|
|
603
|
+
// Add filter complex
|
|
604
|
+
args.push("-filter_complex", filterParts.join(";"));
|
|
605
|
+
// Map outputs
|
|
606
|
+
args.push("-map", "[outv]");
|
|
607
|
+
if (audioStreams.length > 0) {
|
|
608
|
+
args.push("-map", "[outa]");
|
|
609
|
+
}
|
|
610
|
+
// Add encoding settings
|
|
611
|
+
args.push(...presetSettings.ffmpegArgs);
|
|
612
|
+
// Output file
|
|
613
|
+
args.push(outputPath);
|
|
614
|
+
return args;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Run FFmpeg with progress reporting
|
|
618
|
+
*/
|
|
619
|
+
function runFFmpegProcess(ffmpegPath, args, onProgress) {
|
|
620
|
+
return new Promise((resolve, reject) => {
|
|
621
|
+
const ffmpeg = spawn(ffmpegPath, args, {
|
|
622
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
623
|
+
});
|
|
624
|
+
let duration = 0;
|
|
625
|
+
let stderr = "";
|
|
626
|
+
ffmpeg.stderr?.on("data", (data) => {
|
|
627
|
+
const output = data.toString();
|
|
628
|
+
stderr += output;
|
|
629
|
+
// Parse duration
|
|
630
|
+
const durationMatch = output.match(/Duration: (\d+):(\d+):(\d+\.\d+)/);
|
|
631
|
+
if (durationMatch) {
|
|
632
|
+
const [, hours, minutes, seconds] = durationMatch;
|
|
633
|
+
duration =
|
|
634
|
+
parseInt(hours) * 3600 +
|
|
635
|
+
parseInt(minutes) * 60 +
|
|
636
|
+
parseFloat(seconds);
|
|
637
|
+
}
|
|
638
|
+
// Parse progress
|
|
639
|
+
const timeMatch = output.match(/time=(\d+):(\d+):(\d+\.\d+)/);
|
|
640
|
+
if (timeMatch && duration > 0) {
|
|
641
|
+
const [, hours, minutes, seconds] = timeMatch;
|
|
642
|
+
const currentTime = parseInt(hours) * 3600 +
|
|
643
|
+
parseInt(minutes) * 60 +
|
|
644
|
+
parseFloat(seconds);
|
|
645
|
+
const percent = Math.min(100, Math.round((currentTime / duration) * 100));
|
|
646
|
+
onProgress(percent);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
ffmpeg.on("close", (code) => {
|
|
650
|
+
if (code === 0) {
|
|
651
|
+
resolve();
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
// Extract error message
|
|
655
|
+
const errorMatch = stderr.match(/Error.*$/m);
|
|
656
|
+
const errorMsg = errorMatch ? errorMatch[0] : `FFmpeg exited with code ${code}`;
|
|
657
|
+
reject(new Error(errorMsg));
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
ffmpeg.on("error", (err) => {
|
|
661
|
+
reject(err);
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
function getPresetSettings(preset, aspectRatio) {
|
|
666
|
+
const presets = {
|
|
667
|
+
draft: {
|
|
668
|
+
resolution: "640x360",
|
|
669
|
+
videoBitrate: "1M",
|
|
670
|
+
audioBitrate: "128k",
|
|
671
|
+
ffmpegArgs: [
|
|
672
|
+
"-c:v", "libx264",
|
|
673
|
+
"-preset", "ultrafast",
|
|
674
|
+
"-crf", "28",
|
|
675
|
+
"-c:a", "aac",
|
|
676
|
+
"-b:a", "128k",
|
|
677
|
+
],
|
|
678
|
+
},
|
|
679
|
+
standard: {
|
|
680
|
+
resolution: "1280x720",
|
|
681
|
+
videoBitrate: "4M",
|
|
682
|
+
audioBitrate: "192k",
|
|
683
|
+
ffmpegArgs: [
|
|
684
|
+
"-c:v", "libx264",
|
|
685
|
+
"-preset", "medium",
|
|
686
|
+
"-crf", "23",
|
|
687
|
+
"-c:a", "aac",
|
|
688
|
+
"-b:a", "192k",
|
|
689
|
+
],
|
|
690
|
+
},
|
|
691
|
+
high: {
|
|
692
|
+
resolution: "1920x1080",
|
|
693
|
+
videoBitrate: "8M",
|
|
694
|
+
audioBitrate: "256k",
|
|
695
|
+
ffmpegArgs: [
|
|
696
|
+
"-c:v", "libx264",
|
|
697
|
+
"-preset", "slow",
|
|
698
|
+
"-crf", "18",
|
|
699
|
+
"-c:a", "aac",
|
|
700
|
+
"-b:a", "256k",
|
|
701
|
+
],
|
|
702
|
+
},
|
|
703
|
+
ultra: {
|
|
704
|
+
resolution: "3840x2160",
|
|
705
|
+
videoBitrate: "20M",
|
|
706
|
+
audioBitrate: "320k",
|
|
707
|
+
ffmpegArgs: [
|
|
708
|
+
"-c:v", "libx264",
|
|
709
|
+
"-preset", "slow",
|
|
710
|
+
"-crf", "15",
|
|
711
|
+
"-c:a", "aac",
|
|
712
|
+
"-b:a", "320k",
|
|
713
|
+
],
|
|
714
|
+
},
|
|
715
|
+
};
|
|
716
|
+
// Adjust resolution for aspect ratio
|
|
717
|
+
const settings = { ...presets[preset] || presets.standard };
|
|
718
|
+
if (aspectRatio === "9:16") {
|
|
719
|
+
// Vertical video
|
|
720
|
+
const [w, h] = settings.resolution.split("x");
|
|
721
|
+
settings.resolution = `${h}x${w}`;
|
|
722
|
+
}
|
|
723
|
+
else if (aspectRatio === "1:1") {
|
|
724
|
+
// Square video
|
|
725
|
+
const h = settings.resolution.split("x")[1];
|
|
726
|
+
settings.resolution = `${h}x${h}`;
|
|
727
|
+
}
|
|
728
|
+
return settings;
|
|
729
|
+
}
|
|
730
|
+
//# sourceMappingURL=export.js.map
|