@vibeframe/cli 0.54.0 → 0.55.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +63452 -233
- package/package.json +11 -6
- package/dist/agent/adapters/claude.d.ts +0 -15
- package/dist/agent/adapters/claude.d.ts.map +0 -1
- package/dist/agent/adapters/claude.js +0 -119
- package/dist/agent/adapters/claude.js.map +0 -1
- package/dist/agent/adapters/gemini.d.ts +0 -15
- package/dist/agent/adapters/gemini.d.ts.map +0 -1
- package/dist/agent/adapters/gemini.js +0 -132
- package/dist/agent/adapters/gemini.js.map +0 -1
- package/dist/agent/adapters/index.d.ts +0 -28
- package/dist/agent/adapters/index.d.ts.map +0 -1
- package/dist/agent/adapters/index.js +0 -43
- package/dist/agent/adapters/index.js.map +0 -1
- package/dist/agent/adapters/ollama.d.ts +0 -20
- package/dist/agent/adapters/ollama.d.ts.map +0 -1
- package/dist/agent/adapters/ollama.js +0 -186
- package/dist/agent/adapters/ollama.js.map +0 -1
- package/dist/agent/adapters/openai.d.ts +0 -15
- package/dist/agent/adapters/openai.d.ts.map +0 -1
- package/dist/agent/adapters/openai.js +0 -92
- package/dist/agent/adapters/openai.js.map +0 -1
- package/dist/agent/adapters/openrouter.d.ts +0 -16
- package/dist/agent/adapters/openrouter.d.ts.map +0 -1
- package/dist/agent/adapters/openrouter.js +0 -100
- package/dist/agent/adapters/openrouter.js.map +0 -1
- package/dist/agent/adapters/openrouter.test.d.ts +0 -5
- package/dist/agent/adapters/openrouter.test.d.ts.map +0 -1
- package/dist/agent/adapters/openrouter.test.js +0 -73
- package/dist/agent/adapters/openrouter.test.js.map +0 -1
- package/dist/agent/adapters/xai.d.ts +0 -15
- package/dist/agent/adapters/xai.d.ts.map +0 -1
- package/dist/agent/adapters/xai.js +0 -95
- package/dist/agent/adapters/xai.js.map +0 -1
- package/dist/agent/index.d.ts +0 -69
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/index.js +0 -180
- package/dist/agent/index.js.map +0 -1
- package/dist/agent/memory/index.d.ts +0 -70
- package/dist/agent/memory/index.d.ts.map +0 -1
- package/dist/agent/memory/index.js +0 -132
- package/dist/agent/memory/index.js.map +0 -1
- package/dist/agent/prompts/system.d.ts +0 -6
- package/dist/agent/prompts/system.d.ts.map +0 -1
- package/dist/agent/prompts/system.js +0 -103
- package/dist/agent/prompts/system.js.map +0 -1
- package/dist/agent/tools/ai-editing.d.ts +0 -15
- package/dist/agent/tools/ai-editing.d.ts.map +0 -1
- package/dist/agent/tools/ai-editing.js +0 -763
- package/dist/agent/tools/ai-editing.js.map +0 -1
- package/dist/agent/tools/ai-generation.d.ts +0 -13
- package/dist/agent/tools/ai-generation.d.ts.map +0 -1
- package/dist/agent/tools/ai-generation.js +0 -973
- package/dist/agent/tools/ai-generation.js.map +0 -1
- package/dist/agent/tools/ai-pipeline.d.ts +0 -14
- package/dist/agent/tools/ai-pipeline.d.ts.map +0 -1
- package/dist/agent/tools/ai-pipeline.js +0 -961
- package/dist/agent/tools/ai-pipeline.js.map +0 -1
- package/dist/agent/tools/ai.d.ts +0 -13
- package/dist/agent/tools/ai.d.ts.map +0 -1
- package/dist/agent/tools/ai.js +0 -19
- package/dist/agent/tools/ai.js.map +0 -1
- package/dist/agent/tools/batch.d.ts +0 -6
- package/dist/agent/tools/batch.d.ts.map +0 -1
- package/dist/agent/tools/batch.js +0 -383
- package/dist/agent/tools/batch.js.map +0 -1
- package/dist/agent/tools/e2e.test.d.ts +0 -26
- package/dist/agent/tools/e2e.test.d.ts.map +0 -1
- package/dist/agent/tools/e2e.test.js +0 -397
- package/dist/agent/tools/e2e.test.js.map +0 -1
- package/dist/agent/tools/export.d.ts +0 -6
- package/dist/agent/tools/export.d.ts.map +0 -1
- package/dist/agent/tools/export.js +0 -171
- package/dist/agent/tools/export.js.map +0 -1
- package/dist/agent/tools/filesystem.d.ts +0 -6
- package/dist/agent/tools/filesystem.d.ts.map +0 -1
- package/dist/agent/tools/filesystem.js +0 -212
- package/dist/agent/tools/filesystem.js.map +0 -1
- package/dist/agent/tools/index.d.ts +0 -66
- package/dist/agent/tools/index.d.ts.map +0 -1
- package/dist/agent/tools/index.js +0 -123
- package/dist/agent/tools/index.js.map +0 -1
- package/dist/agent/tools/integration.test.d.ts +0 -11
- package/dist/agent/tools/integration.test.d.ts.map +0 -1
- package/dist/agent/tools/integration.test.js +0 -659
- package/dist/agent/tools/integration.test.js.map +0 -1
- package/dist/agent/tools/media.d.ts +0 -6
- package/dist/agent/tools/media.d.ts.map +0 -1
- package/dist/agent/tools/media.js +0 -616
- package/dist/agent/tools/media.js.map +0 -1
- package/dist/agent/tools/project.d.ts +0 -6
- package/dist/agent/tools/project.d.ts.map +0 -1
- package/dist/agent/tools/project.js +0 -284
- package/dist/agent/tools/project.js.map +0 -1
- package/dist/agent/tools/scene.d.ts +0 -17
- package/dist/agent/tools/scene.d.ts.map +0 -1
- package/dist/agent/tools/scene.js +0 -263
- package/dist/agent/tools/scene.js.map +0 -1
- package/dist/agent/tools/scene.test.d.ts +0 -2
- package/dist/agent/tools/scene.test.d.ts.map +0 -1
- package/dist/agent/tools/scene.test.js +0 -139
- package/dist/agent/tools/scene.test.js.map +0 -1
- package/dist/agent/tools/timeline.d.ts +0 -6
- package/dist/agent/tools/timeline.d.ts.map +0 -1
- package/dist/agent/tools/timeline.js +0 -875
- package/dist/agent/tools/timeline.js.map +0 -1
- package/dist/agent/types.d.ts +0 -59
- package/dist/agent/types.d.ts.map +0 -1
- package/dist/agent/types.js +0 -5
- package/dist/agent/types.js.map +0 -1
- package/dist/commands/_shared/scene-html-emit.d.ts +0 -120
- package/dist/commands/_shared/scene-html-emit.d.ts.map +0 -1
- package/dist/commands/_shared/scene-html-emit.js +0 -422
- package/dist/commands/_shared/scene-html-emit.js.map +0 -1
- package/dist/commands/_shared/scene-html-emit.test.d.ts +0 -2
- package/dist/commands/_shared/scene-html-emit.test.d.ts.map +0 -1
- package/dist/commands/_shared/scene-html-emit.test.js +0 -396
- package/dist/commands/_shared/scene-html-emit.test.js.map +0 -1
- package/dist/commands/_shared/scene-lint.d.ts +0 -110
- package/dist/commands/_shared/scene-lint.d.ts.map +0 -1
- package/dist/commands/_shared/scene-lint.js +0 -207
- package/dist/commands/_shared/scene-lint.js.map +0 -1
- package/dist/commands/_shared/scene-lint.test.d.ts +0 -2
- package/dist/commands/_shared/scene-lint.test.d.ts.map +0 -1
- package/dist/commands/_shared/scene-lint.test.js +0 -208
- package/dist/commands/_shared/scene-lint.test.js.map +0 -1
- package/dist/commands/_shared/scene-project.d.ts +0 -101
- package/dist/commands/_shared/scene-project.d.ts.map +0 -1
- package/dist/commands/_shared/scene-project.js +0 -281
- package/dist/commands/_shared/scene-project.js.map +0 -1
- package/dist/commands/_shared/scene-project.test.d.ts +0 -2
- package/dist/commands/_shared/scene-project.test.d.ts.map +0 -1
- package/dist/commands/_shared/scene-project.test.js +0 -203
- package/dist/commands/_shared/scene-project.test.js.map +0 -1
- package/dist/commands/_shared/scene-render.d.ts +0 -79
- package/dist/commands/_shared/scene-render.d.ts.map +0 -1
- package/dist/commands/_shared/scene-render.js +0 -138
- package/dist/commands/_shared/scene-render.js.map +0 -1
- package/dist/commands/_shared/scene-render.test.d.ts +0 -2
- package/dist/commands/_shared/scene-render.test.d.ts.map +0 -1
- package/dist/commands/_shared/scene-render.test.js +0 -145
- package/dist/commands/_shared/scene-render.test.js.map +0 -1
- package/dist/commands/_shared/segments-to-scenes.d.ts +0 -85
- package/dist/commands/_shared/segments-to-scenes.d.ts.map +0 -1
- package/dist/commands/_shared/segments-to-scenes.js +0 -202
- package/dist/commands/_shared/segments-to-scenes.js.map +0 -1
- package/dist/commands/_shared/segments-to-scenes.test.d.ts +0 -2
- package/dist/commands/_shared/segments-to-scenes.test.d.ts.map +0 -1
- package/dist/commands/_shared/segments-to-scenes.test.js +0 -219
- package/dist/commands/_shared/segments-to-scenes.test.js.map +0 -1
- package/dist/commands/_shared/tts-resolve.d.ts +0 -75
- package/dist/commands/_shared/tts-resolve.d.ts.map +0 -1
- package/dist/commands/_shared/tts-resolve.js +0 -89
- package/dist/commands/_shared/tts-resolve.js.map +0 -1
- package/dist/commands/_shared/tts-resolve.test.d.ts +0 -6
- package/dist/commands/_shared/tts-resolve.test.d.ts.map +0 -1
- package/dist/commands/_shared/tts-resolve.test.js +0 -147
- package/dist/commands/_shared/tts-resolve.test.js.map +0 -1
- package/dist/commands/agent.d.ts +0 -21
- package/dist/commands/agent.d.ts.map +0 -1
- package/dist/commands/agent.js +0 -285
- package/dist/commands/agent.js.map +0 -1
- package/dist/commands/ai-analyze.d.ts +0 -106
- package/dist/commands/ai-analyze.d.ts.map +0 -1
- package/dist/commands/ai-analyze.js +0 -320
- package/dist/commands/ai-analyze.js.map +0 -1
- package/dist/commands/ai-animated-caption.d.ts +0 -64
- package/dist/commands/ai-animated-caption.d.ts.map +0 -1
- package/dist/commands/ai-animated-caption.js +0 -272
- package/dist/commands/ai-animated-caption.js.map +0 -1
- package/dist/commands/ai-audio.d.ts +0 -98
- package/dist/commands/ai-audio.d.ts.map +0 -1
- package/dist/commands/ai-audio.js +0 -1059
- package/dist/commands/ai-audio.js.map +0 -1
- package/dist/commands/ai-broll.d.ts +0 -15
- package/dist/commands/ai-broll.d.ts.map +0 -1
- package/dist/commands/ai-broll.js +0 -421
- package/dist/commands/ai-broll.js.map +0 -1
- package/dist/commands/ai-edit-cli.d.ts +0 -14
- package/dist/commands/ai-edit-cli.d.ts.map +0 -1
- package/dist/commands/ai-edit-cli.js +0 -585
- package/dist/commands/ai-edit-cli.js.map +0 -1
- package/dist/commands/ai-edit.d.ts +0 -398
- package/dist/commands/ai-edit.d.ts.map +0 -1
- package/dist/commands/ai-edit.js +0 -1060
- package/dist/commands/ai-edit.js.map +0 -1
- package/dist/commands/ai-fill-gaps.d.ts +0 -14
- package/dist/commands/ai-fill-gaps.d.ts.map +0 -1
- package/dist/commands/ai-fill-gaps.js +0 -447
- package/dist/commands/ai-fill-gaps.js.map +0 -1
- package/dist/commands/ai-helpers.d.ts +0 -20
- package/dist/commands/ai-helpers.d.ts.map +0 -1
- package/dist/commands/ai-helpers.js +0 -59
- package/dist/commands/ai-helpers.js.map +0 -1
- package/dist/commands/ai-highlights.d.ts +0 -127
- package/dist/commands/ai-highlights.d.ts.map +0 -1
- package/dist/commands/ai-highlights.js +0 -1053
- package/dist/commands/ai-highlights.js.map +0 -1
- package/dist/commands/ai-image.d.ts +0 -76
- package/dist/commands/ai-image.d.ts.map +0 -1
- package/dist/commands/ai-image.js +0 -858
- package/dist/commands/ai-image.js.map +0 -1
- package/dist/commands/ai-motion.d.ts +0 -50
- package/dist/commands/ai-motion.d.ts.map +0 -1
- package/dist/commands/ai-motion.js +0 -297
- package/dist/commands/ai-motion.js.map +0 -1
- package/dist/commands/ai-narrate.d.ts +0 -66
- package/dist/commands/ai-narrate.d.ts.map +0 -1
- package/dist/commands/ai-narrate.js +0 -332
- package/dist/commands/ai-narrate.js.map +0 -1
- package/dist/commands/ai-review.d.ts +0 -57
- package/dist/commands/ai-review.d.ts.map +0 -1
- package/dist/commands/ai-review.js +0 -270
- package/dist/commands/ai-review.js.map +0 -1
- package/dist/commands/ai-script-pipeline-cli.d.ts +0 -9
- package/dist/commands/ai-script-pipeline-cli.d.ts.map +0 -1
- package/dist/commands/ai-script-pipeline-cli.js +0 -502
- package/dist/commands/ai-script-pipeline-cli.js.map +0 -1
- package/dist/commands/ai-script-pipeline.d.ts +0 -305
- package/dist/commands/ai-script-pipeline.d.ts.map +0 -1
- package/dist/commands/ai-script-pipeline.js +0 -1546
- package/dist/commands/ai-script-pipeline.js.map +0 -1
- package/dist/commands/ai-suggest-edit.d.ts +0 -14
- package/dist/commands/ai-suggest-edit.d.ts.map +0 -1
- package/dist/commands/ai-suggest-edit.js +0 -215
- package/dist/commands/ai-suggest-edit.js.map +0 -1
- package/dist/commands/ai-video-fx.d.ts +0 -14
- package/dist/commands/ai-video-fx.d.ts.map +0 -1
- package/dist/commands/ai-video-fx.js +0 -396
- package/dist/commands/ai-video-fx.js.map +0 -1
- package/dist/commands/ai-video.d.ts +0 -91
- package/dist/commands/ai-video.d.ts.map +0 -1
- package/dist/commands/ai-video.js +0 -1055
- package/dist/commands/ai-video.js.map +0 -1
- package/dist/commands/ai-viral.d.ts +0 -15
- package/dist/commands/ai-viral.d.ts.map +0 -1
- package/dist/commands/ai-viral.js +0 -507
- package/dist/commands/ai-viral.js.map +0 -1
- package/dist/commands/ai-visual-fx.d.ts +0 -14
- package/dist/commands/ai-visual-fx.d.ts.map +0 -1
- package/dist/commands/ai-visual-fx.js +0 -521
- package/dist/commands/ai-visual-fx.js.map +0 -1
- package/dist/commands/ai.d.ts +0 -38
- package/dist/commands/ai.d.ts.map +0 -1
- package/dist/commands/ai.js +0 -225
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/ai.test.d.ts +0 -2
- package/dist/commands/ai.test.d.ts.map +0 -1
- package/dist/commands/ai.test.js +0 -557
- package/dist/commands/ai.test.js.map +0 -1
- package/dist/commands/analyze.d.ts +0 -16
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js +0 -247
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/audio.d.ts +0 -18
- package/dist/commands/audio.d.ts.map +0 -1
- package/dist/commands/audio.js +0 -539
- package/dist/commands/audio.js.map +0 -1
- package/dist/commands/batch.d.ts +0 -3
- package/dist/commands/batch.d.ts.map +0 -1
- package/dist/commands/batch.js +0 -419
- package/dist/commands/batch.js.map +0 -1
- package/dist/commands/batch.test.d.ts +0 -2
- package/dist/commands/batch.test.d.ts.map +0 -1
- package/dist/commands/batch.test.js +0 -203
- package/dist/commands/batch.test.js.map +0 -1
- package/dist/commands/context.d.ts +0 -7
- package/dist/commands/context.d.ts.map +0 -1
- package/dist/commands/context.js +0 -66
- package/dist/commands/context.js.map +0 -1
- package/dist/commands/demo.d.ts +0 -7
- package/dist/commands/demo.d.ts.map +0 -1
- package/dist/commands/demo.js +0 -192
- package/dist/commands/demo.js.map +0 -1
- package/dist/commands/detect.d.ts +0 -56
- package/dist/commands/detect.d.ts.map +0 -1
- package/dist/commands/detect.js +0 -464
- package/dist/commands/detect.js.map +0 -1
- package/dist/commands/doctor.d.ts +0 -6
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js +0 -322
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/edit-cmd.d.ts +0 -112
- package/dist/commands/edit-cmd.d.ts.map +0 -1
- package/dist/commands/edit-cmd.js +0 -1041
- package/dist/commands/edit-cmd.js.map +0 -1
- package/dist/commands/export.d.ts +0 -61
- package/dist/commands/export.d.ts.map +0 -1
- package/dist/commands/export.js +0 -980
- package/dist/commands/export.js.map +0 -1
- package/dist/commands/export.test.d.ts +0 -2
- package/dist/commands/export.test.d.ts.map +0 -1
- package/dist/commands/export.test.js +0 -107
- package/dist/commands/export.test.js.map +0 -1
- package/dist/commands/generate.d.ts +0 -84
- package/dist/commands/generate.d.ts.map +0 -1
- package/dist/commands/generate.js +0 -2030
- package/dist/commands/generate.js.map +0 -1
- package/dist/commands/media.d.ts +0 -3
- package/dist/commands/media.d.ts.map +0 -1
- package/dist/commands/media.js +0 -163
- package/dist/commands/media.js.map +0 -1
- package/dist/commands/output.d.ts +0 -51
- package/dist/commands/output.d.ts.map +0 -1
- package/dist/commands/output.js +0 -219
- package/dist/commands/output.js.map +0 -1
- package/dist/commands/output.test.d.ts +0 -2
- package/dist/commands/output.test.d.ts.map +0 -1
- package/dist/commands/output.test.js +0 -151
- package/dist/commands/output.test.js.map +0 -1
- package/dist/commands/pipeline.d.ts +0 -19
- package/dist/commands/pipeline.d.ts.map +0 -1
- package/dist/commands/pipeline.js +0 -345
- package/dist/commands/pipeline.js.map +0 -1
- package/dist/commands/project.d.ts +0 -3
- package/dist/commands/project.d.ts.map +0 -1
- package/dist/commands/project.js +0 -181
- package/dist/commands/project.js.map +0 -1
- package/dist/commands/project.test.d.ts +0 -2
- package/dist/commands/project.test.d.ts.map +0 -1
- package/dist/commands/project.test.js +0 -105
- package/dist/commands/project.test.js.map +0 -1
- package/dist/commands/run.d.ts +0 -15
- package/dist/commands/run.d.ts.map +0 -1
- package/dist/commands/run.js +0 -204
- package/dist/commands/run.js.map +0 -1
- package/dist/commands/sanitize.d.ts +0 -21
- package/dist/commands/sanitize.d.ts.map +0 -1
- package/dist/commands/sanitize.js +0 -56
- package/dist/commands/sanitize.js.map +0 -1
- package/dist/commands/scene.d.ts +0 -86
- package/dist/commands/scene.d.ts.map +0 -1
- package/dist/commands/scene.js +0 -719
- package/dist/commands/scene.js.map +0 -1
- package/dist/commands/scene.test.d.ts +0 -2
- package/dist/commands/scene.test.d.ts.map +0 -1
- package/dist/commands/scene.test.js +0 -219
- package/dist/commands/scene.test.js.map +0 -1
- package/dist/commands/schema.d.ts +0 -13
- package/dist/commands/schema.d.ts.map +0 -1
- package/dist/commands/schema.js +0 -218
- package/dist/commands/schema.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -6
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -461
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/timeline.d.ts +0 -3
- package/dist/commands/timeline.d.ts.map +0 -1
- package/dist/commands/timeline.js +0 -609
- package/dist/commands/timeline.js.map +0 -1
- package/dist/commands/timeline.test.d.ts +0 -2
- package/dist/commands/timeline.test.d.ts.map +0 -1
- package/dist/commands/timeline.test.js +0 -334
- package/dist/commands/timeline.test.js.map +0 -1
- package/dist/commands/validate.d.ts +0 -34
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/validate.js +0 -70
- package/dist/commands/validate.js.map +0 -1
- package/dist/config/config.test.d.ts +0 -2
- package/dist/config/config.test.d.ts.map +0 -1
- package/dist/config/config.test.js +0 -164
- package/dist/config/config.test.js.map +0 -1
- package/dist/config/index.d.ts +0 -35
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js +0 -101
- package/dist/config/index.js.map +0 -1
- package/dist/config/schema.d.ts +0 -52
- package/dist/config/schema.d.ts.map +0 -1
- package/dist/config/schema.js +0 -44
- package/dist/config/schema.js.map +0 -1
- package/dist/engine/index.d.ts +0 -3
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -2
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/project.d.ts +0 -84
- package/dist/engine/project.d.ts.map +0 -1
- package/dist/engine/project.js +0 -355
- package/dist/engine/project.js.map +0 -1
- package/dist/engine/project.test.d.ts +0 -2
- package/dist/engine/project.test.d.ts.map +0 -1
- package/dist/engine/project.test.js +0 -599
- package/dist/engine/project.test.js.map +0 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/pipeline/executor.d.ts +0 -19
- package/dist/pipeline/executor.d.ts.map +0 -1
- package/dist/pipeline/executor.js +0 -397
- package/dist/pipeline/executor.js.map +0 -1
- package/dist/pipeline/index.d.ts +0 -4
- package/dist/pipeline/index.d.ts.map +0 -1
- package/dist/pipeline/index.js +0 -2
- package/dist/pipeline/index.js.map +0 -1
- package/dist/pipeline/renderers/__tests__/html-template.test.d.ts +0 -2
- package/dist/pipeline/renderers/__tests__/html-template.test.d.ts.map +0 -1
- package/dist/pipeline/renderers/__tests__/html-template.test.js +0 -170
- package/dist/pipeline/renderers/__tests__/html-template.test.js.map +0 -1
- package/dist/pipeline/renderers/__tests__/integration.test.d.ts +0 -2
- package/dist/pipeline/renderers/__tests__/integration.test.d.ts.map +0 -1
- package/dist/pipeline/renderers/__tests__/integration.test.js +0 -175
- package/dist/pipeline/renderers/__tests__/integration.test.js.map +0 -1
- package/dist/pipeline/renderers/chrome.d.ts +0 -26
- package/dist/pipeline/renderers/chrome.d.ts.map +0 -1
- package/dist/pipeline/renderers/chrome.js +0 -48
- package/dist/pipeline/renderers/chrome.js.map +0 -1
- package/dist/pipeline/renderers/html-clips.d.ts +0 -20
- package/dist/pipeline/renderers/html-clips.d.ts.map +0 -1
- package/dist/pipeline/renderers/html-clips.js +0 -50
- package/dist/pipeline/renderers/html-clips.js.map +0 -1
- package/dist/pipeline/renderers/html-runtime.d.ts +0 -7
- package/dist/pipeline/renderers/html-runtime.d.ts.map +0 -1
- package/dist/pipeline/renderers/html-runtime.js +0 -80
- package/dist/pipeline/renderers/html-runtime.js.map +0 -1
- package/dist/pipeline/renderers/html-template.d.ts +0 -3
- package/dist/pipeline/renderers/html-template.d.ts.map +0 -1
- package/dist/pipeline/renderers/html-template.js +0 -36
- package/dist/pipeline/renderers/html-template.js.map +0 -1
- package/dist/pipeline/renderers/hyperframes.d.ts +0 -8
- package/dist/pipeline/renderers/hyperframes.d.ts.map +0 -1
- package/dist/pipeline/renderers/hyperframes.js +0 -63
- package/dist/pipeline/renderers/hyperframes.js.map +0 -1
- package/dist/pipeline/renderers/project-builder.d.ts +0 -8
- package/dist/pipeline/renderers/project-builder.d.ts.map +0 -1
- package/dist/pipeline/renderers/project-builder.js +0 -65
- package/dist/pipeline/renderers/project-builder.js.map +0 -1
- package/dist/pipeline/renderers/types.d.ts +0 -41
- package/dist/pipeline/renderers/types.d.ts.map +0 -1
- package/dist/pipeline/renderers/types.js +0 -11
- package/dist/pipeline/renderers/types.js.map +0 -1
- package/dist/pipeline/resolver.d.ts +0 -20
- package/dist/pipeline/resolver.d.ts.map +0 -1
- package/dist/pipeline/resolver.js +0 -74
- package/dist/pipeline/resolver.js.map +0 -1
- package/dist/pipeline/types.d.ts +0 -82
- package/dist/pipeline/types.d.ts.map +0 -1
- package/dist/pipeline/types.js +0 -8
- package/dist/pipeline/types.js.map +0 -1
- package/dist/utils/api-key.d.ts +0 -33
- package/dist/utils/api-key.d.ts.map +0 -1
- package/dist/utils/api-key.js +0 -228
- package/dist/utils/api-key.js.map +0 -1
- package/dist/utils/api-key.test.d.ts +0 -2
- package/dist/utils/api-key.test.d.ts.map +0 -1
- package/dist/utils/api-key.test.js +0 -35
- package/dist/utils/api-key.test.js.map +0 -1
- package/dist/utils/audio.d.ts +0 -23
- package/dist/utils/audio.d.ts.map +0 -1
- package/dist/utils/audio.js +0 -79
- package/dist/utils/audio.js.map +0 -1
- package/dist/utils/exec-safe.d.ts +0 -22
- package/dist/utils/exec-safe.d.ts.map +0 -1
- package/dist/utils/exec-safe.js +0 -62
- package/dist/utils/exec-safe.js.map +0 -1
- package/dist/utils/first-run.d.ts +0 -17
- package/dist/utils/first-run.d.ts.map +0 -1
- package/dist/utils/first-run.js +0 -73
- package/dist/utils/first-run.js.map +0 -1
- package/dist/utils/input-validation.d.ts +0 -27
- package/dist/utils/input-validation.d.ts.map +0 -1
- package/dist/utils/input-validation.js +0 -67
- package/dist/utils/input-validation.js.map +0 -1
- package/dist/utils/provider-resolver.d.ts +0 -19
- package/dist/utils/provider-resolver.d.ts.map +0 -1
- package/dist/utils/provider-resolver.js +0 -75
- package/dist/utils/provider-resolver.js.map +0 -1
- package/dist/utils/remotion.d.ts +0 -231
- package/dist/utils/remotion.d.ts.map +0 -1
- package/dist/utils/remotion.js +0 -879
- package/dist/utils/remotion.js.map +0 -1
- package/dist/utils/subtitle.d.ts +0 -65
- package/dist/utils/subtitle.d.ts.map +0 -1
- package/dist/utils/subtitle.js +0 -135
- package/dist/utils/subtitle.js.map +0 -1
- package/dist/utils/subtitle.test.d.ts +0 -2
- package/dist/utils/subtitle.test.d.ts.map +0 -1
- package/dist/utils/subtitle.test.js +0 -175
- package/dist/utils/subtitle.test.js.map +0 -1
- package/dist/utils/tty.d.ts +0 -45
- package/dist/utils/tty.d.ts.map +0 -1
- package/dist/utils/tty.js +0 -231
- package/dist/utils/tty.js.map +0 -1
package/dist/commands/export.js
DELETED
|
@@ -1,980 +0,0 @@
|
|
|
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
|
-
import { exitWithError, generalError, notFoundError, outputResult, usageError } from "./output.js";
|
|
10
|
-
import { validateOutputPath } from "./validate.js";
|
|
11
|
-
/**
|
|
12
|
-
* Resolve project file path - handles both file paths and directory paths
|
|
13
|
-
* If path is a directory, looks for project.vibe.json inside
|
|
14
|
-
*/
|
|
15
|
-
async function resolveProjectPath(inputPath) {
|
|
16
|
-
const filePath = resolve(process.cwd(), inputPath);
|
|
17
|
-
try {
|
|
18
|
-
const stats = await stat(filePath);
|
|
19
|
-
if (stats.isDirectory()) {
|
|
20
|
-
return resolve(filePath, "project.vibe.json");
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
// Path doesn't exist or other error - let readFile handle it
|
|
25
|
-
}
|
|
26
|
-
return filePath;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Get the duration of a media file using ffprobe
|
|
30
|
-
* For images, returns a default duration since they have no inherent time
|
|
31
|
-
*/
|
|
32
|
-
export async function getMediaDuration(filePath, mediaType, defaultImageDuration = 5) {
|
|
33
|
-
if (mediaType === "image") {
|
|
34
|
-
return defaultImageDuration;
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
return await ffprobeDuration(filePath);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return defaultImageDuration;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Check if a media file has an audio stream
|
|
45
|
-
*/
|
|
46
|
-
export async function checkHasAudio(filePath) {
|
|
47
|
-
try {
|
|
48
|
-
const { stdout } = await execSafe("ffprobe", [
|
|
49
|
-
"-v", "error", "-select_streams", "a", "-show_entries", "stream=codec_type", "-of", "default=noprint_wrappers=1:nokey=1", filePath,
|
|
50
|
-
]);
|
|
51
|
-
return stdout.trim().length > 0;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const CODEC_MAP = {
|
|
58
|
-
h264: "libx264",
|
|
59
|
-
h265: "libx265",
|
|
60
|
-
vp9: "libvpx-vp9",
|
|
61
|
-
};
|
|
62
|
-
export const BITRATE_PATTERN = /^\d+(?:\.\d+)?[kKmMgG]?$/;
|
|
63
|
-
export const RESOLUTION_PATTERN = /^\d+x\d+$/;
|
|
64
|
-
export const VALID_CODECS = ["h264", "h265", "vp9"];
|
|
65
|
-
export function validateOverrides(overrides) {
|
|
66
|
-
if (overrides.bitrate !== undefined && !BITRATE_PATTERN.test(overrides.bitrate)) {
|
|
67
|
-
return `Invalid --bitrate "${overrides.bitrate}". Use digits with optional k/M suffix (e.g. 5000k, 8M).`;
|
|
68
|
-
}
|
|
69
|
-
if (overrides.fps !== undefined) {
|
|
70
|
-
if (!Number.isFinite(overrides.fps) || overrides.fps <= 0 || overrides.fps > 240) {
|
|
71
|
-
return `Invalid --fps "${overrides.fps}". Use a positive number up to 240.`;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (overrides.resolution !== undefined && !RESOLUTION_PATTERN.test(overrides.resolution)) {
|
|
75
|
-
return `Invalid --resolution "${overrides.resolution}". Use <width>x<height> (e.g. 1920x1080).`;
|
|
76
|
-
}
|
|
77
|
-
if (overrides.codec !== undefined && !VALID_CODECS.includes(overrides.codec)) {
|
|
78
|
-
return `Invalid --codec "${overrides.codec}". Supported: ${VALID_CODECS.join(", ")}.`;
|
|
79
|
-
}
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Reusable export function for programmatic usage
|
|
84
|
-
*/
|
|
85
|
-
export async function runExport(projectPath, outputPath, options = {}) {
|
|
86
|
-
const { preset = "standard", format = "mp4", overwrite = false, gapFill = "extend", bitrate, fps, resolution, codec } = options;
|
|
87
|
-
const overrideError = validateOverrides({ bitrate, fps, resolution, codec });
|
|
88
|
-
if (overrideError) {
|
|
89
|
-
return { success: false, message: overrideError };
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
// Check if FFmpeg is installed
|
|
93
|
-
const ffmpegPath = await findFFmpeg();
|
|
94
|
-
if (!ffmpegPath) {
|
|
95
|
-
return {
|
|
96
|
-
success: false,
|
|
97
|
-
message: "FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)",
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
// Load project
|
|
101
|
-
const filePath = await resolveProjectPath(projectPath);
|
|
102
|
-
const content = await readFile(filePath, "utf-8");
|
|
103
|
-
const data = JSON.parse(content);
|
|
104
|
-
const project = Project.fromJSON(data);
|
|
105
|
-
const summary = project.getSummary();
|
|
106
|
-
if (summary.clipCount === 0) {
|
|
107
|
-
return {
|
|
108
|
-
success: false,
|
|
109
|
-
message: "Project has no clips to export",
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
// Determine output path
|
|
113
|
-
const finalOutputPath = resolve(process.cwd(), outputPath);
|
|
114
|
-
// Get preset settings
|
|
115
|
-
const basePresetSettings = getPresetSettings(preset, summary.aspectRatio);
|
|
116
|
-
const presetSettings = applyCustomOverrides(basePresetSettings, { bitrate, fps, resolution, codec });
|
|
117
|
-
// Get clips sorted by start time
|
|
118
|
-
const clips = project.getClips().sort((a, b) => a.startTime - b.startTime);
|
|
119
|
-
const sources = project.getSources();
|
|
120
|
-
// Verify source files exist, check audio streams, and measure actual durations
|
|
121
|
-
const sourceAudioMap = new Map();
|
|
122
|
-
const sourceActualDurationMap = new Map();
|
|
123
|
-
for (const clip of clips) {
|
|
124
|
-
const source = sources.find((s) => s.id === clip.sourceId);
|
|
125
|
-
if (source) {
|
|
126
|
-
try {
|
|
127
|
-
await access(source.url);
|
|
128
|
-
if (!sourceActualDurationMap.has(source.id)) {
|
|
129
|
-
try {
|
|
130
|
-
const dur = await getMediaDuration(source.url, source.type);
|
|
131
|
-
if (dur > 0)
|
|
132
|
-
sourceActualDurationMap.set(source.id, dur);
|
|
133
|
-
}
|
|
134
|
-
catch { /* fall back to metadata */ }
|
|
135
|
-
}
|
|
136
|
-
if (source.type === "video" && !sourceAudioMap.has(source.id)) {
|
|
137
|
-
sourceAudioMap.set(source.id, await checkHasAudio(source.url));
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
return {
|
|
142
|
-
success: false,
|
|
143
|
-
message: `Source file not found: ${source.url}`,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// Build FFmpeg command
|
|
149
|
-
const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, finalOutputPath, { overwrite, format, gapFill }, sourceAudioMap, sourceActualDurationMap);
|
|
150
|
-
// Run FFmpeg
|
|
151
|
-
await runFFmpegProcess(ffmpegPath, ffmpegArgs, () => { });
|
|
152
|
-
return {
|
|
153
|
-
success: true,
|
|
154
|
-
message: `Exported: ${outputPath}`,
|
|
155
|
-
outputPath: finalOutputPath,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
160
|
-
return {
|
|
161
|
-
success: false,
|
|
162
|
-
message: `Export failed: ${errorMessage}`,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
export const exportCommand = new Command("export")
|
|
167
|
-
.description("Export project to video file")
|
|
168
|
-
.argument("<project>", "Project file path")
|
|
169
|
-
.option("-o, --output <path>", "Output file path")
|
|
170
|
-
.option("-f, --format <format>", "Output format (mp4, webm, mov, gif)", "mp4")
|
|
171
|
-
.option("-p, --preset <preset>", "Quality preset (draft, standard, high, ultra)", "standard")
|
|
172
|
-
.option("-y, --overwrite", "Overwrite output file if exists", false)
|
|
173
|
-
.option("-g, --gap-fill <strategy>", "Gap filling strategy (black, extend)", "extend")
|
|
174
|
-
.option("--backend <name>", "Render backend: ffmpeg (default) | hyperframes (experimental)", "ffmpeg")
|
|
175
|
-
.option("--bitrate <value>", "Video bitrate (e.g. 5000k, 8M) — overrides preset")
|
|
176
|
-
.option("--fps <number>", "Frames per second (e.g. 24, 30, 60) — overrides preset")
|
|
177
|
-
.option("--resolution <WxH>", "Output resolution (e.g. 1920x1080) — overrides preset")
|
|
178
|
-
.option("--codec <codec>", "Video codec: h264 (default) | h265 | vp9 — overrides preset")
|
|
179
|
-
.option("--dry-run", "Preview parameters without executing")
|
|
180
|
-
.addHelpText("after", `
|
|
181
|
-
Examples:
|
|
182
|
-
$ vibe export project.vibe.json -o output.mp4
|
|
183
|
-
$ vibe export project.vibe.json -o output.mp4 -p high -y
|
|
184
|
-
$ vibe export project.vibe.json -o output.webm -f webm
|
|
185
|
-
$ vibe export project.vibe.json -o output.gif -f gif
|
|
186
|
-
$ vibe export project.vibe.json -o out.mp4 --bitrate 5000k --fps 24 --codec h265
|
|
187
|
-
$ vibe export project.vibe.json -o out.mp4 -p high --fps 60
|
|
188
|
-
|
|
189
|
-
Cost: Free (no API keys needed). Requires FFmpeg.
|
|
190
|
-
GIF format: 15fps, no audio, looping. Good for previews and sharing.
|
|
191
|
-
Custom flags (--bitrate, --fps, --resolution, --codec) override preset values.
|
|
192
|
-
Run 'vibe schema export' for structured parameter info.`)
|
|
193
|
-
.action(async (projectPath, options) => {
|
|
194
|
-
const spinner = ora("Checking FFmpeg...").start();
|
|
195
|
-
try {
|
|
196
|
-
if (options.output) {
|
|
197
|
-
validateOutputPath(options.output);
|
|
198
|
-
}
|
|
199
|
-
const customOverrides = {
|
|
200
|
-
bitrate: options.bitrate,
|
|
201
|
-
fps: options.fps !== undefined ? Number(options.fps) : undefined,
|
|
202
|
-
resolution: options.resolution,
|
|
203
|
-
codec: options.codec,
|
|
204
|
-
};
|
|
205
|
-
const overrideError = validateOverrides(customOverrides);
|
|
206
|
-
if (overrideError) {
|
|
207
|
-
spinner.stop();
|
|
208
|
-
exitWithError(usageError(overrideError));
|
|
209
|
-
}
|
|
210
|
-
if (options.dryRun) {
|
|
211
|
-
outputResult({
|
|
212
|
-
dryRun: true,
|
|
213
|
-
command: "export",
|
|
214
|
-
params: {
|
|
215
|
-
project: projectPath,
|
|
216
|
-
output: options.output || null,
|
|
217
|
-
format: options.format,
|
|
218
|
-
preset: options.preset,
|
|
219
|
-
overwrite: options.overwrite,
|
|
220
|
-
gapFill: options.gapFill,
|
|
221
|
-
backend: options.backend,
|
|
222
|
-
bitrate: options.bitrate ?? null,
|
|
223
|
-
fps: customOverrides.fps ?? null,
|
|
224
|
-
resolution: options.resolution ?? null,
|
|
225
|
-
codec: options.codec ?? null,
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
// Hyperframes backend path
|
|
231
|
-
if (options.backend === "hyperframes") {
|
|
232
|
-
await runHyperframesExport(projectPath, options, spinner);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
// Check if FFmpeg is installed
|
|
236
|
-
const ffmpegPath = await findFFmpeg();
|
|
237
|
-
if (!ffmpegPath) {
|
|
238
|
-
spinner.fail("FFmpeg not found");
|
|
239
|
-
exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS), apt install ffmpeg (Linux), or winget install ffmpeg (Windows)"));
|
|
240
|
-
}
|
|
241
|
-
// Load project
|
|
242
|
-
spinner.text = "Loading project...";
|
|
243
|
-
const filePath = await resolveProjectPath(projectPath);
|
|
244
|
-
const content = await readFile(filePath, "utf-8");
|
|
245
|
-
const data = JSON.parse(content);
|
|
246
|
-
const project = Project.fromJSON(data);
|
|
247
|
-
const summary = project.getSummary();
|
|
248
|
-
if (summary.clipCount === 0) {
|
|
249
|
-
spinner.fail("Project has no clips to export");
|
|
250
|
-
exitWithError(usageError("Project has no clips to export"));
|
|
251
|
-
}
|
|
252
|
-
// Determine output path
|
|
253
|
-
const outputPath = options.output
|
|
254
|
-
? resolve(process.cwd(), options.output)
|
|
255
|
-
: resolve(process.cwd(), `${basename(projectPath, ".vibe.json")}.${options.format}`);
|
|
256
|
-
// Get preset settings + apply custom overrides
|
|
257
|
-
const basePresetSettings = getPresetSettings(options.preset, summary.aspectRatio);
|
|
258
|
-
const presetSettings = applyCustomOverrides(basePresetSettings, customOverrides);
|
|
259
|
-
// Get clips sorted by start time
|
|
260
|
-
const clips = project.getClips().sort((a, b) => a.startTime - b.startTime);
|
|
261
|
-
const sources = project.getSources();
|
|
262
|
-
// Verify source files exist, check audio streams, and measure actual durations
|
|
263
|
-
spinner.text = "Verifying source files...";
|
|
264
|
-
const sourceAudioMap = new Map();
|
|
265
|
-
const sourceActualDurationMap = new Map();
|
|
266
|
-
for (const clip of clips) {
|
|
267
|
-
const source = sources.find((s) => s.id === clip.sourceId);
|
|
268
|
-
if (source) {
|
|
269
|
-
try {
|
|
270
|
-
await access(source.url);
|
|
271
|
-
if (!sourceActualDurationMap.has(source.id)) {
|
|
272
|
-
try {
|
|
273
|
-
const dur = await getMediaDuration(source.url, source.type);
|
|
274
|
-
if (dur > 0)
|
|
275
|
-
sourceActualDurationMap.set(source.id, dur);
|
|
276
|
-
}
|
|
277
|
-
catch { /* fall back to metadata */ }
|
|
278
|
-
}
|
|
279
|
-
if (source.type === "video" && !sourceAudioMap.has(source.id)) {
|
|
280
|
-
sourceAudioMap.set(source.id, await checkHasAudio(source.url));
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
spinner.fail(`Source file not found: ${source.url}`);
|
|
285
|
-
exitWithError(notFoundError(source.url));
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
// Build FFmpeg command
|
|
290
|
-
spinner.text = "Building export command...";
|
|
291
|
-
const gapFillStrategy = (options.gapFill === "black" ? "black" : "extend");
|
|
292
|
-
const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, outputPath, { ...options, gapFill: gapFillStrategy }, sourceAudioMap, sourceActualDurationMap);
|
|
293
|
-
if (process.env.DEBUG) {
|
|
294
|
-
console.log("\nFFmpeg command:");
|
|
295
|
-
console.log("ffmpeg", ffmpegArgs.join(" "));
|
|
296
|
-
console.log();
|
|
297
|
-
}
|
|
298
|
-
// Run FFmpeg
|
|
299
|
-
spinner.text = "Encoding...";
|
|
300
|
-
await runFFmpegProcess(ffmpegPath, ffmpegArgs, (progress) => {
|
|
301
|
-
spinner.text = `Encoding... ${progress}%`;
|
|
302
|
-
});
|
|
303
|
-
spinner.succeed(chalk.green(`Exported: ${outputPath}`));
|
|
304
|
-
console.log();
|
|
305
|
-
console.log(chalk.dim(" Duration:"), `${summary.duration.toFixed(1)}s`);
|
|
306
|
-
console.log(chalk.dim(" Clips:"), summary.clipCount);
|
|
307
|
-
console.log(chalk.dim(" Format:"), options.format);
|
|
308
|
-
console.log(chalk.dim(" Preset:"), options.preset);
|
|
309
|
-
console.log(chalk.dim(" Resolution:"), presetSettings.resolution);
|
|
310
|
-
if (options.bitrate)
|
|
311
|
-
console.log(chalk.dim(" Bitrate:"), options.bitrate);
|
|
312
|
-
if (options.fps)
|
|
313
|
-
console.log(chalk.dim(" FPS:"), options.fps);
|
|
314
|
-
if (options.codec)
|
|
315
|
-
console.log(chalk.dim(" Codec:"), options.codec);
|
|
316
|
-
console.log();
|
|
317
|
-
}
|
|
318
|
-
catch (error) {
|
|
319
|
-
spinner.fail("Export failed");
|
|
320
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
321
|
-
exitWithError(generalError(`Export failed: ${msg}`));
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
/**
|
|
325
|
-
* Find FFmpeg executable
|
|
326
|
-
*/
|
|
327
|
-
async function findFFmpeg() {
|
|
328
|
-
try {
|
|
329
|
-
const { stdout } = await execSafe("which", ["ffmpeg"]);
|
|
330
|
-
return stdout.trim().split("\n")[0];
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
try {
|
|
334
|
-
const { stdout } = await execSafe("where", ["ffmpeg"]);
|
|
335
|
-
return stdout.trim().split("\n")[0];
|
|
336
|
-
}
|
|
337
|
-
catch {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Detect gaps in timeline between clips
|
|
344
|
-
* Returns array of gaps with start and end times
|
|
345
|
-
*/
|
|
346
|
-
function detectTimelineGaps(clips, totalDuration) {
|
|
347
|
-
if (clips.length === 0)
|
|
348
|
-
return [];
|
|
349
|
-
const gaps = [];
|
|
350
|
-
const sortedClips = [...clips].sort((a, b) => a.startTime - b.startTime);
|
|
351
|
-
// Check for gap at the start (first clip doesn't start at 0)
|
|
352
|
-
if (sortedClips[0].startTime > 0.001) {
|
|
353
|
-
gaps.push({ start: 0, end: sortedClips[0].startTime });
|
|
354
|
-
}
|
|
355
|
-
// Check for gaps between clips
|
|
356
|
-
for (let i = 0; i < sortedClips.length - 1; i++) {
|
|
357
|
-
const clipEnd = sortedClips[i].startTime + sortedClips[i].duration;
|
|
358
|
-
const nextStart = sortedClips[i + 1].startTime;
|
|
359
|
-
// Allow small tolerance for floating point errors
|
|
360
|
-
if (nextStart > clipEnd + 0.001) {
|
|
361
|
-
gaps.push({ start: clipEnd, end: nextStart });
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
// Check for gap at the end if totalDuration is provided
|
|
365
|
-
if (totalDuration !== undefined) {
|
|
366
|
-
const lastClip = sortedClips[sortedClips.length - 1];
|
|
367
|
-
const lastClipEnd = lastClip.startTime + lastClip.duration;
|
|
368
|
-
if (totalDuration > lastClipEnd + 0.001) {
|
|
369
|
-
gaps.push({ start: lastClipEnd, end: totalDuration });
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
return gaps;
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Create gap fill plans by extending adjacent clips
|
|
376
|
-
* Priority:
|
|
377
|
-
* 1. Extend clip AFTER the gap backwards (if sourceStartOffset > 0)
|
|
378
|
-
* 2. Extend clip BEFORE the gap forwards (if source has unused duration)
|
|
379
|
-
* 3. Fallback to black frames
|
|
380
|
-
*/
|
|
381
|
-
function createGapFillPlans(gaps, clips, sources) {
|
|
382
|
-
const sortedClips = [...clips].sort((a, b) => a.startTime - b.startTime);
|
|
383
|
-
return gaps.map((gap) => {
|
|
384
|
-
const fills = [];
|
|
385
|
-
let remainingStart = gap.start;
|
|
386
|
-
let remainingEnd = gap.end;
|
|
387
|
-
// Find clip AFTER the gap (for extending backwards)
|
|
388
|
-
const clipAfter = sortedClips.find((c) => Math.abs(c.startTime - gap.end) < 0.01);
|
|
389
|
-
// Find clip BEFORE the gap (for extending forwards)
|
|
390
|
-
const clipBefore = sortedClips.find((c) => Math.abs((c.startTime + c.duration) - gap.start) < 0.01);
|
|
391
|
-
// Try extending clip after the gap backwards first
|
|
392
|
-
if (clipAfter && clipAfter.sourceStartOffset > 0.01) {
|
|
393
|
-
const source = sources.find((s) => s.id === clipAfter.sourceId);
|
|
394
|
-
if (source && source.type === "video") {
|
|
395
|
-
const availableExtension = clipAfter.sourceStartOffset;
|
|
396
|
-
const extensionDuration = Math.min(availableExtension, remainingEnd - remainingStart);
|
|
397
|
-
if (extensionDuration > 0.01) {
|
|
398
|
-
// Extend from the gap end backwards
|
|
399
|
-
const fillStart = remainingEnd - extensionDuration;
|
|
400
|
-
const sourceStart = clipAfter.sourceStartOffset - extensionDuration;
|
|
401
|
-
const sourceEnd = clipAfter.sourceStartOffset;
|
|
402
|
-
fills.push({
|
|
403
|
-
type: "extend-after",
|
|
404
|
-
sourceId: source.id,
|
|
405
|
-
sourceUrl: source.url,
|
|
406
|
-
start: fillStart,
|
|
407
|
-
end: remainingEnd,
|
|
408
|
-
sourceStart,
|
|
409
|
-
sourceEnd,
|
|
410
|
-
});
|
|
411
|
-
remainingEnd = fillStart;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
// If there's still a gap, try extending clip before the gap forwards
|
|
416
|
-
if (remainingEnd - remainingStart > 0.01 && clipBefore) {
|
|
417
|
-
const source = sources.find((s) => s.id === clipBefore.sourceId);
|
|
418
|
-
if (source && source.type === "video") {
|
|
419
|
-
const usedEndInSource = clipBefore.sourceEndOffset;
|
|
420
|
-
const availableExtension = source.duration - usedEndInSource;
|
|
421
|
-
if (availableExtension > 0.01) {
|
|
422
|
-
const extensionDuration = Math.min(availableExtension, remainingEnd - remainingStart);
|
|
423
|
-
if (extensionDuration > 0.01) {
|
|
424
|
-
const sourceStart = usedEndInSource;
|
|
425
|
-
const sourceEnd = usedEndInSource + extensionDuration;
|
|
426
|
-
fills.push({
|
|
427
|
-
type: "extend-before",
|
|
428
|
-
sourceId: source.id,
|
|
429
|
-
sourceUrl: source.url,
|
|
430
|
-
start: remainingStart,
|
|
431
|
-
end: remainingStart + extensionDuration,
|
|
432
|
-
sourceStart,
|
|
433
|
-
sourceEnd,
|
|
434
|
-
});
|
|
435
|
-
remainingStart = remainingStart + extensionDuration;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
// Fill any remaining gap with black
|
|
441
|
-
if (remainingEnd - remainingStart > 0.01) {
|
|
442
|
-
fills.push({
|
|
443
|
-
type: "black",
|
|
444
|
-
start: remainingStart,
|
|
445
|
-
end: remainingEnd,
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
return { gap, fills };
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Build FFmpeg arguments for export
|
|
453
|
-
*/
|
|
454
|
-
function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, sourceAudioMap = new Map(), sourceActualDurationMap = new Map()) {
|
|
455
|
-
const args = [];
|
|
456
|
-
// Overwrite flag first
|
|
457
|
-
if (options.overwrite) {
|
|
458
|
-
args.push("-y");
|
|
459
|
-
}
|
|
460
|
-
// Add input files
|
|
461
|
-
const sourceMap = new Map();
|
|
462
|
-
let inputIndex = 0;
|
|
463
|
-
for (const clip of clips) {
|
|
464
|
-
const source = sources.find((s) => s.id === clip.sourceId);
|
|
465
|
-
if (source && !sourceMap.has(source.id)) {
|
|
466
|
-
// Add -loop 1 before image inputs to create a continuous video stream
|
|
467
|
-
if (source.type === "image") {
|
|
468
|
-
args.push("-loop", "1");
|
|
469
|
-
}
|
|
470
|
-
args.push("-i", source.url);
|
|
471
|
-
sourceMap.set(source.id, inputIndex);
|
|
472
|
-
inputIndex++;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
// Build filter complex
|
|
476
|
-
const filterParts = [];
|
|
477
|
-
// Separate clips by track type for proper timeline-based export
|
|
478
|
-
// Get track info to determine clip types
|
|
479
|
-
const videoClips = clips.filter((clip) => {
|
|
480
|
-
const source = sources.find((s) => s.id === clip.sourceId);
|
|
481
|
-
return source && (source.type === "image" || source.type === "video");
|
|
482
|
-
}).sort((a, b) => a.startTime - b.startTime);
|
|
483
|
-
// Include audio clips from:
|
|
484
|
-
// 1. Explicit audio sources (narration, music)
|
|
485
|
-
// 2. Video sources when there are NO separate audio clips (e.g., highlight reels)
|
|
486
|
-
const explicitAudioClips = clips.filter((clip) => {
|
|
487
|
-
const source = sources.find((s) => s.id === clip.sourceId);
|
|
488
|
-
return source && source.type === "audio";
|
|
489
|
-
}).sort((a, b) => a.startTime - b.startTime);
|
|
490
|
-
// If no explicit audio clips, extract audio from video clips
|
|
491
|
-
const audioClips = explicitAudioClips.length > 0
|
|
492
|
-
? explicitAudioClips
|
|
493
|
-
: clips.filter((clip) => {
|
|
494
|
-
const source = sources.find((s) => s.id === clip.sourceId);
|
|
495
|
-
return source && source.type === "video";
|
|
496
|
-
}).sort((a, b) => a.startTime - b.startTime);
|
|
497
|
-
// Get target resolution for scaling (all clips must match for concat)
|
|
498
|
-
const [targetWidth, targetHeight] = presetSettings.resolution.split("x").map(Number);
|
|
499
|
-
// Detect gaps in video timeline
|
|
500
|
-
// For totalDuration, use the longest audio clip end time if explicit audio exists
|
|
501
|
-
// (audio is usually the reference for timing in b-roll scenarios)
|
|
502
|
-
let totalDuration;
|
|
503
|
-
if (explicitAudioClips.length > 0) {
|
|
504
|
-
const audioEnd = Math.max(...explicitAudioClips.map(c => c.startTime + c.duration));
|
|
505
|
-
totalDuration = audioEnd;
|
|
506
|
-
}
|
|
507
|
-
const videoGaps = detectTimelineGaps(videoClips, totalDuration);
|
|
508
|
-
// Create gap fill plans based on strategy
|
|
509
|
-
const gapFillStrategy = options.gapFill || "extend";
|
|
510
|
-
const gapFillPlans = gapFillStrategy === "extend"
|
|
511
|
-
? createGapFillPlans(videoGaps, videoClips, sources)
|
|
512
|
-
: videoGaps.map((gap) => ({
|
|
513
|
-
gap,
|
|
514
|
-
fills: [{ type: "black", start: gap.start, end: gap.end }],
|
|
515
|
-
}));
|
|
516
|
-
const videoSegments = [];
|
|
517
|
-
// Add video clips as segments
|
|
518
|
-
for (const clip of videoClips) {
|
|
519
|
-
videoSegments.push({ type: 'clip', clip, startTime: clip.startTime });
|
|
520
|
-
}
|
|
521
|
-
// Add gap fills as segments (from gap fill plans)
|
|
522
|
-
for (const plan of gapFillPlans) {
|
|
523
|
-
for (const fill of plan.fills) {
|
|
524
|
-
if (fill.type === "black") {
|
|
525
|
-
videoSegments.push({
|
|
526
|
-
type: 'black',
|
|
527
|
-
startTime: fill.start,
|
|
528
|
-
duration: fill.end - fill.start,
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
// extend-before or extend-after
|
|
533
|
-
videoSegments.push({
|
|
534
|
-
type: 'extended',
|
|
535
|
-
sourceId: fill.sourceId,
|
|
536
|
-
sourceUrl: fill.sourceUrl,
|
|
537
|
-
startTime: fill.start,
|
|
538
|
-
duration: fill.end - fill.start,
|
|
539
|
-
sourceStart: fill.sourceStart,
|
|
540
|
-
sourceEnd: fill.sourceEnd,
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
// Sort by start time
|
|
546
|
-
videoSegments.sort((a, b) => a.startTime - b.startTime);
|
|
547
|
-
// Process video segments (clips, extended clips, and black frames)
|
|
548
|
-
const videoStreams = [];
|
|
549
|
-
let videoStreamIdx = 0;
|
|
550
|
-
for (const segment of videoSegments) {
|
|
551
|
-
if (segment.type === 'clip' && segment.clip) {
|
|
552
|
-
const clip = segment.clip;
|
|
553
|
-
const source = sources.find((s) => s.id === clip.sourceId);
|
|
554
|
-
if (!source)
|
|
555
|
-
continue;
|
|
556
|
-
const srcIdx = sourceMap.get(source.id);
|
|
557
|
-
if (srcIdx === undefined)
|
|
558
|
-
continue;
|
|
559
|
-
// Video filter chain - images need different handling than video
|
|
560
|
-
let videoFilter;
|
|
561
|
-
if (source.type === "image") {
|
|
562
|
-
// Images: trim from 0 to clip duration (no source offset since images are looped)
|
|
563
|
-
videoFilter = `[${srcIdx}:v]trim=start=0:end=${clip.duration},setpts=PTS-STARTPTS`;
|
|
564
|
-
}
|
|
565
|
-
else {
|
|
566
|
-
// Video: use source offsets
|
|
567
|
-
const trimStart = clip.sourceStartOffset;
|
|
568
|
-
const trimEnd = clip.sourceStartOffset + clip.duration;
|
|
569
|
-
videoFilter = `[${srcIdx}:v]trim=start=${trimStart}:end=${trimEnd},setpts=PTS-STARTPTS`;
|
|
570
|
-
// If video source is shorter than clip duration, freeze last frame to fill
|
|
571
|
-
// This prevents black frames when narration is longer than generated video
|
|
572
|
-
// Use actual measured duration (ffprobe) over project metadata (may be stale)
|
|
573
|
-
const sourceDuration = sourceActualDurationMap.get(source.id) || source.duration || 0;
|
|
574
|
-
const availableDuration = sourceDuration - clip.sourceStartOffset;
|
|
575
|
-
if (availableDuration > 0 && availableDuration < clip.duration - 0.1) {
|
|
576
|
-
const padDuration = clip.duration - availableDuration;
|
|
577
|
-
videoFilter += `,tpad=stop_mode=clone:stop_duration=${padDuration.toFixed(3)}`;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
// Scale to target resolution for concat compatibility (force same size, pad if needed)
|
|
581
|
-
videoFilter += `,scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
|
|
582
|
-
// Apply effects
|
|
583
|
-
for (const effect of clip.effects || []) {
|
|
584
|
-
if (effect.type === "fadeIn") {
|
|
585
|
-
videoFilter += `,fade=t=in:st=0:d=${effect.duration}`;
|
|
586
|
-
}
|
|
587
|
-
else if (effect.type === "fadeOut") {
|
|
588
|
-
const fadeStart = clip.duration - effect.duration;
|
|
589
|
-
videoFilter += `,fade=t=out:st=${fadeStart}:d=${effect.duration}`;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
videoFilter += `[v${videoStreamIdx}]`;
|
|
593
|
-
filterParts.push(videoFilter);
|
|
594
|
-
videoStreams.push(`[v${videoStreamIdx}]`);
|
|
595
|
-
videoStreamIdx++;
|
|
596
|
-
}
|
|
597
|
-
else if (segment.type === 'extended' && segment.sourceId) {
|
|
598
|
-
// Extended segment - use source video to fill gap
|
|
599
|
-
const srcIdx = sourceMap.get(segment.sourceId);
|
|
600
|
-
if (srcIdx === undefined) {
|
|
601
|
-
// Fallback to black if source not found in input map
|
|
602
|
-
const gapFilter = `color=c=black:s=${targetWidth}x${targetHeight}:d=${segment.duration}:r=30,format=yuv420p[v${videoStreamIdx}]`;
|
|
603
|
-
filterParts.push(gapFilter);
|
|
604
|
-
videoStreams.push(`[v${videoStreamIdx}]`);
|
|
605
|
-
videoStreamIdx++;
|
|
606
|
-
continue;
|
|
607
|
-
}
|
|
608
|
-
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}]`;
|
|
609
|
-
filterParts.push(videoFilter);
|
|
610
|
-
videoStreams.push(`[v${videoStreamIdx}]`);
|
|
611
|
-
videoStreamIdx++;
|
|
612
|
-
}
|
|
613
|
-
else if (segment.type === 'black') {
|
|
614
|
-
// Generate black frame for the gap duration
|
|
615
|
-
const gapFilter = `color=c=black:s=${targetWidth}x${targetHeight}:d=${segment.duration}:r=30,format=yuv420p[v${videoStreamIdx}]`;
|
|
616
|
-
filterParts.push(gapFilter);
|
|
617
|
-
videoStreams.push(`[v${videoStreamIdx}]`);
|
|
618
|
-
videoStreamIdx++;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
// ── Multi-track audio processing ────────────────────────────────────
|
|
622
|
-
// Group audio clips by trackId, build each track as a separate stream,
|
|
623
|
-
// then mix all tracks together with amix.
|
|
624
|
-
// Step 1: Group audio clips by track
|
|
625
|
-
const audioTrackMap = new Map();
|
|
626
|
-
for (const clip of audioClips) {
|
|
627
|
-
const trackId = clip.trackId || "audio-track-1";
|
|
628
|
-
if (!audioTrackMap.has(trackId)) {
|
|
629
|
-
audioTrackMap.set(trackId, []);
|
|
630
|
-
}
|
|
631
|
-
audioTrackMap.get(trackId).push(clip);
|
|
632
|
-
}
|
|
633
|
-
// Compute total timeline duration for gap detection
|
|
634
|
-
const videoDuration = videoClips.length > 0
|
|
635
|
-
? Math.max(...videoClips.map(c => c.startTime + c.duration))
|
|
636
|
-
: 0;
|
|
637
|
-
const audioDuration = audioClips.length > 0
|
|
638
|
-
? Math.max(...audioClips.map(c => c.startTime + c.duration))
|
|
639
|
-
: 0;
|
|
640
|
-
const timelineDuration = totalDuration || Math.max(videoDuration, audioDuration);
|
|
641
|
-
// Step 2: Build each track as a concat stream
|
|
642
|
-
const trackOutputLabels = [];
|
|
643
|
-
let audioStreamIdx = 0;
|
|
644
|
-
for (const [, trackClips] of audioTrackMap) {
|
|
645
|
-
const sorted = [...trackClips].sort((a, b) => a.startTime - b.startTime);
|
|
646
|
-
const trackGaps = detectTimelineGaps(sorted, timelineDuration);
|
|
647
|
-
const segments = [];
|
|
648
|
-
for (const clip of sorted) {
|
|
649
|
-
segments.push({ type: 'clip', clip, startTime: clip.startTime });
|
|
650
|
-
}
|
|
651
|
-
for (const gap of trackGaps) {
|
|
652
|
-
segments.push({ type: 'gap', gap, startTime: gap.start });
|
|
653
|
-
}
|
|
654
|
-
segments.sort((a, b) => a.startTime - b.startTime);
|
|
655
|
-
// Process segments into filter chains
|
|
656
|
-
const segmentLabels = [];
|
|
657
|
-
for (const segment of segments) {
|
|
658
|
-
if (segment.type === 'clip' && segment.clip) {
|
|
659
|
-
const clip = segment.clip;
|
|
660
|
-
const source = sources.find((s) => s.id === clip.sourceId);
|
|
661
|
-
if (!source)
|
|
662
|
-
continue;
|
|
663
|
-
const srcIdx = sourceMap.get(source.id);
|
|
664
|
-
if (srcIdx === undefined)
|
|
665
|
-
continue;
|
|
666
|
-
const hasAudio = source.type === "audio" || sourceAudioMap.get(source.id) === true;
|
|
667
|
-
let audioFilter;
|
|
668
|
-
if (hasAudio) {
|
|
669
|
-
const audioTrimStart = clip.sourceStartOffset;
|
|
670
|
-
const audioTrimEnd = clip.sourceStartOffset + clip.duration;
|
|
671
|
-
const sourceDuration = source.duration || 0;
|
|
672
|
-
const clipDuration = clip.duration;
|
|
673
|
-
if (source.type === "audio" && sourceDuration > clipDuration && audioTrimStart === 0) {
|
|
674
|
-
const tempo = sourceDuration / clipDuration;
|
|
675
|
-
if (tempo <= 2.0) {
|
|
676
|
-
audioFilter = `[${srcIdx}:a]atempo=${tempo.toFixed(4)},asetpts=PTS-STARTPTS`;
|
|
677
|
-
}
|
|
678
|
-
else {
|
|
679
|
-
audioFilter = `[${srcIdx}:a]atrim=start=${audioTrimStart}:end=${audioTrimEnd},asetpts=PTS-STARTPTS`;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
else {
|
|
683
|
-
audioFilter = `[${srcIdx}:a]atrim=start=${audioTrimStart}:end=${audioTrimEnd},asetpts=PTS-STARTPTS`;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
else {
|
|
687
|
-
audioFilter = `anullsrc=r=48000:cl=stereo,atrim=0:${clip.duration},asetpts=PTS-STARTPTS`;
|
|
688
|
-
}
|
|
689
|
-
// Apply per-clip volume (0.0-1.0)
|
|
690
|
-
const clipVolume = clip.volume;
|
|
691
|
-
if (clipVolume !== undefined && clipVolume !== 1.0) {
|
|
692
|
-
audioFilter += `,volume=${clipVolume.toFixed(2)}`;
|
|
693
|
-
}
|
|
694
|
-
// Apply audio effects (fade, volume effect)
|
|
695
|
-
for (const effect of clip.effects || []) {
|
|
696
|
-
if (effect.type === "fadeIn") {
|
|
697
|
-
audioFilter += `,afade=t=in:st=0:d=${effect.duration}`;
|
|
698
|
-
}
|
|
699
|
-
else if (effect.type === "fadeOut") {
|
|
700
|
-
const fadeStart = clip.duration - effect.duration;
|
|
701
|
-
audioFilter += `,afade=t=out:st=${fadeStart}:d=${effect.duration}`;
|
|
702
|
-
}
|
|
703
|
-
else if (effect.type === "volume" && effect.params?.level !== undefined) {
|
|
704
|
-
audioFilter += `,volume=${effect.params.level}`;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
const label = `a${audioStreamIdx}`;
|
|
708
|
-
audioFilter += `[${label}]`;
|
|
709
|
-
filterParts.push(audioFilter);
|
|
710
|
-
segmentLabels.push(`[${label}]`);
|
|
711
|
-
audioStreamIdx++;
|
|
712
|
-
}
|
|
713
|
-
else if (segment.type === 'gap' && segment.gap) {
|
|
714
|
-
const gapDuration = segment.gap.end - segment.gap.start;
|
|
715
|
-
if (gapDuration > 0.001) {
|
|
716
|
-
const label = `a${audioStreamIdx}`;
|
|
717
|
-
filterParts.push(`anullsrc=r=48000:cl=stereo,atrim=0:${gapDuration.toFixed(4)},asetpts=PTS-STARTPTS[${label}]`);
|
|
718
|
-
segmentLabels.push(`[${label}]`);
|
|
719
|
-
audioStreamIdx++;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
// Concat this track's segments into one stream
|
|
724
|
-
if (segmentLabels.length > 1) {
|
|
725
|
-
const trackLabel = `atrack${trackOutputLabels.length}`;
|
|
726
|
-
filterParts.push(`${segmentLabels.join("")}concat=n=${segmentLabels.length}:v=0:a=1[${trackLabel}]`);
|
|
727
|
-
trackOutputLabels.push(`[${trackLabel}]`);
|
|
728
|
-
}
|
|
729
|
-
else if (segmentLabels.length === 1) {
|
|
730
|
-
// Single segment — rename to track label
|
|
731
|
-
const trackLabel = `atrack${trackOutputLabels.length}`;
|
|
732
|
-
filterParts.push(`${segmentLabels[0]}acopy[${trackLabel}]`);
|
|
733
|
-
trackOutputLabels.push(`[${trackLabel}]`);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
// Concatenate video clips
|
|
737
|
-
if (videoStreams.length > 1) {
|
|
738
|
-
filterParts.push(`${videoStreams.join("")}concat=n=${videoStreams.length}:v=1:a=0[outv]`);
|
|
739
|
-
}
|
|
740
|
-
else if (videoStreams.length === 1) {
|
|
741
|
-
filterParts.push(`${videoStreams[0]}copy[outv]`);
|
|
742
|
-
}
|
|
743
|
-
// Step 3: Mix all audio tracks together
|
|
744
|
-
// amix with normalize=0 prevents auto-volume reduction
|
|
745
|
-
if (trackOutputLabels.length > 1) {
|
|
746
|
-
filterParts.push(`${trackOutputLabels.join("")}amix=inputs=${trackOutputLabels.length}:duration=longest:normalize=0[outa]`);
|
|
747
|
-
}
|
|
748
|
-
else if (trackOutputLabels.length === 1) {
|
|
749
|
-
filterParts.push(`${trackOutputLabels[0]}acopy[outa]`);
|
|
750
|
-
}
|
|
751
|
-
// Add filter complex
|
|
752
|
-
args.push("-filter_complex", filterParts.join(";"));
|
|
753
|
-
// Map outputs
|
|
754
|
-
args.push("-map", "[outv]");
|
|
755
|
-
if (trackOutputLabels.length > 0) {
|
|
756
|
-
args.push("-map", "[outa]");
|
|
757
|
-
}
|
|
758
|
-
// Add encoding settings
|
|
759
|
-
if (options.format === "gif") {
|
|
760
|
-
// GIF: drop audio track (GIF has no audio)
|
|
761
|
-
const audioIdx = args.indexOf("[outa]");
|
|
762
|
-
if (audioIdx !== -1) {
|
|
763
|
-
const mapIdx = args.lastIndexOf("-map", audioIdx);
|
|
764
|
-
if (mapIdx !== -1)
|
|
765
|
-
args.splice(mapIdx, 2);
|
|
766
|
-
}
|
|
767
|
-
args.push("-r", "15"); // 15fps for reasonable file size
|
|
768
|
-
args.push("-loop", "0"); // loop forever
|
|
769
|
-
}
|
|
770
|
-
else {
|
|
771
|
-
args.push(...presetSettings.ffmpegArgs);
|
|
772
|
-
}
|
|
773
|
-
// Output file
|
|
774
|
-
args.push(outputPath);
|
|
775
|
-
return args;
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* Run FFmpeg with progress reporting
|
|
779
|
-
*/
|
|
780
|
-
function runFFmpegProcess(ffmpegPath, args, onProgress) {
|
|
781
|
-
return new Promise((resolve, reject) => {
|
|
782
|
-
const ffmpeg = spawn(ffmpegPath, args, {
|
|
783
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
784
|
-
});
|
|
785
|
-
let duration = 0;
|
|
786
|
-
let stderr = "";
|
|
787
|
-
ffmpeg.stderr?.on("data", (data) => {
|
|
788
|
-
const output = data.toString();
|
|
789
|
-
stderr += output;
|
|
790
|
-
// Parse duration
|
|
791
|
-
const durationMatch = output.match(/Duration: (\d+):(\d+):(\d+\.\d+)/);
|
|
792
|
-
if (durationMatch) {
|
|
793
|
-
const [, hours, minutes, seconds] = durationMatch;
|
|
794
|
-
duration =
|
|
795
|
-
parseInt(hours) * 3600 +
|
|
796
|
-
parseInt(minutes) * 60 +
|
|
797
|
-
parseFloat(seconds);
|
|
798
|
-
}
|
|
799
|
-
// Parse progress
|
|
800
|
-
const timeMatch = output.match(/time=(\d+):(\d+):(\d+\.\d+)/);
|
|
801
|
-
if (timeMatch && duration > 0) {
|
|
802
|
-
const [, hours, minutes, seconds] = timeMatch;
|
|
803
|
-
const currentTime = parseInt(hours) * 3600 +
|
|
804
|
-
parseInt(minutes) * 60 +
|
|
805
|
-
parseFloat(seconds);
|
|
806
|
-
const percent = Math.min(100, Math.round((currentTime / duration) * 100));
|
|
807
|
-
onProgress(percent);
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
ffmpeg.on("close", (code) => {
|
|
811
|
-
if (code === 0) {
|
|
812
|
-
resolve();
|
|
813
|
-
}
|
|
814
|
-
else {
|
|
815
|
-
// Extract error message
|
|
816
|
-
const errorMatch = stderr.match(/Error.*$/m);
|
|
817
|
-
const errorMsg = errorMatch ? errorMatch[0] : `FFmpeg exited with code ${code}`;
|
|
818
|
-
reject(new Error(errorMsg));
|
|
819
|
-
}
|
|
820
|
-
});
|
|
821
|
-
ffmpeg.on("error", (err) => {
|
|
822
|
-
reject(err);
|
|
823
|
-
});
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
export function applyCustomOverrides(settings, overrides) {
|
|
827
|
-
const result = {
|
|
828
|
-
...settings,
|
|
829
|
-
ffmpegArgs: [...settings.ffmpegArgs],
|
|
830
|
-
};
|
|
831
|
-
if (overrides.resolution) {
|
|
832
|
-
result.resolution = overrides.resolution;
|
|
833
|
-
}
|
|
834
|
-
if (overrides.codec) {
|
|
835
|
-
const ffmpegCodec = CODEC_MAP[overrides.codec];
|
|
836
|
-
const cvIdx = result.ffmpegArgs.indexOf("-c:v");
|
|
837
|
-
if (cvIdx !== -1) {
|
|
838
|
-
result.ffmpegArgs[cvIdx + 1] = ffmpegCodec;
|
|
839
|
-
}
|
|
840
|
-
else {
|
|
841
|
-
result.ffmpegArgs.unshift("-c:v", ffmpegCodec);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
if (overrides.bitrate) {
|
|
845
|
-
const crfIdx = result.ffmpegArgs.indexOf("-crf");
|
|
846
|
-
if (crfIdx !== -1) {
|
|
847
|
-
result.ffmpegArgs.splice(crfIdx, 2);
|
|
848
|
-
}
|
|
849
|
-
const bvIdx = result.ffmpegArgs.indexOf("-b:v");
|
|
850
|
-
if (bvIdx !== -1) {
|
|
851
|
-
result.ffmpegArgs[bvIdx + 1] = overrides.bitrate;
|
|
852
|
-
}
|
|
853
|
-
else {
|
|
854
|
-
result.ffmpegArgs.push("-b:v", overrides.bitrate);
|
|
855
|
-
}
|
|
856
|
-
result.videoBitrate = overrides.bitrate;
|
|
857
|
-
}
|
|
858
|
-
if (overrides.fps !== undefined) {
|
|
859
|
-
const rIdx = result.ffmpegArgs.indexOf("-r");
|
|
860
|
-
const fpsValue = String(overrides.fps);
|
|
861
|
-
if (rIdx !== -1) {
|
|
862
|
-
result.ffmpegArgs[rIdx + 1] = fpsValue;
|
|
863
|
-
}
|
|
864
|
-
else {
|
|
865
|
-
result.ffmpegArgs.push("-r", fpsValue);
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
return result;
|
|
869
|
-
}
|
|
870
|
-
function getPresetSettings(preset, aspectRatio) {
|
|
871
|
-
const presets = {
|
|
872
|
-
draft: {
|
|
873
|
-
resolution: "640x360",
|
|
874
|
-
videoBitrate: "1M",
|
|
875
|
-
audioBitrate: "128k",
|
|
876
|
-
ffmpegArgs: [
|
|
877
|
-
"-c:v", "libx264",
|
|
878
|
-
"-preset", "ultrafast",
|
|
879
|
-
"-crf", "28",
|
|
880
|
-
"-c:a", "aac",
|
|
881
|
-
"-b:a", "128k",
|
|
882
|
-
],
|
|
883
|
-
},
|
|
884
|
-
standard: {
|
|
885
|
-
resolution: "1280x720",
|
|
886
|
-
videoBitrate: "4M",
|
|
887
|
-
audioBitrate: "192k",
|
|
888
|
-
ffmpegArgs: [
|
|
889
|
-
"-c:v", "libx264",
|
|
890
|
-
"-preset", "medium",
|
|
891
|
-
"-crf", "23",
|
|
892
|
-
"-c:a", "aac",
|
|
893
|
-
"-b:a", "192k",
|
|
894
|
-
],
|
|
895
|
-
},
|
|
896
|
-
high: {
|
|
897
|
-
resolution: "1920x1080",
|
|
898
|
-
videoBitrate: "8M",
|
|
899
|
-
audioBitrate: "256k",
|
|
900
|
-
ffmpegArgs: [
|
|
901
|
-
"-c:v", "libx264",
|
|
902
|
-
"-preset", "slow",
|
|
903
|
-
"-crf", "18",
|
|
904
|
-
"-c:a", "aac",
|
|
905
|
-
"-b:a", "256k",
|
|
906
|
-
],
|
|
907
|
-
},
|
|
908
|
-
ultra: {
|
|
909
|
-
resolution: "3840x2160",
|
|
910
|
-
videoBitrate: "20M",
|
|
911
|
-
audioBitrate: "320k",
|
|
912
|
-
ffmpegArgs: [
|
|
913
|
-
"-c:v", "libx264",
|
|
914
|
-
"-preset", "slow",
|
|
915
|
-
"-crf", "15",
|
|
916
|
-
"-c:a", "aac",
|
|
917
|
-
"-b:a", "320k",
|
|
918
|
-
],
|
|
919
|
-
},
|
|
920
|
-
};
|
|
921
|
-
// Adjust resolution for aspect ratio
|
|
922
|
-
const settings = { ...presets[preset] || presets.standard };
|
|
923
|
-
if (aspectRatio === "9:16") {
|
|
924
|
-
// Vertical video
|
|
925
|
-
const [w, h] = settings.resolution.split("x");
|
|
926
|
-
settings.resolution = `${h}x${w}`;
|
|
927
|
-
}
|
|
928
|
-
else if (aspectRatio === "1:1") {
|
|
929
|
-
// Square video
|
|
930
|
-
const h = settings.resolution.split("x")[1];
|
|
931
|
-
settings.resolution = `${h}x${h}`;
|
|
932
|
-
}
|
|
933
|
-
return settings;
|
|
934
|
-
}
|
|
935
|
-
async function runHyperframesExport(projectPath, options, spinner) {
|
|
936
|
-
spinner.text = "Loading project...";
|
|
937
|
-
const { readFile } = await import("node:fs/promises");
|
|
938
|
-
const { resolve, basename } = await import("node:path");
|
|
939
|
-
const { Project } = await import("../engine/index.js");
|
|
940
|
-
const { createHyperframesBackend } = await import("../pipeline/renderers/hyperframes.js");
|
|
941
|
-
const { exitWithError, generalError, outputResult } = await import("./output.js");
|
|
942
|
-
const chalk = (await import("chalk")).default;
|
|
943
|
-
const filePath = resolve(process.cwd(), projectPath);
|
|
944
|
-
const content = await readFile(filePath, "utf-8");
|
|
945
|
-
const project = Project.fromJSON(JSON.parse(content));
|
|
946
|
-
const state = project.getState();
|
|
947
|
-
const outputPath = options.output
|
|
948
|
-
? resolve(process.cwd(), options.output)
|
|
949
|
-
: resolve(process.cwd(), `${basename(projectPath, ".vibe.json")}.${options.format ?? "mp4"}`);
|
|
950
|
-
const quality = (["draft", "standard", "high"].includes(options.preset ?? "")
|
|
951
|
-
? options.preset
|
|
952
|
-
: "standard");
|
|
953
|
-
const backend = createHyperframesBackend();
|
|
954
|
-
spinner.text = "Rendering with Hyperframes...";
|
|
955
|
-
const result = await backend.render({
|
|
956
|
-
projectState: state,
|
|
957
|
-
outputPath,
|
|
958
|
-
fps: 30,
|
|
959
|
-
quality,
|
|
960
|
-
format: (options.format ?? "mp4"),
|
|
961
|
-
onProgress: (pct, stage) => { spinner.text = `[hyperframes] ${stage} ${pct}%`; },
|
|
962
|
-
});
|
|
963
|
-
if (!result.success) {
|
|
964
|
-
spinner.fail("Hyperframes render failed");
|
|
965
|
-
exitWithError(generalError(`Hyperframes render failed: ${result.error}`));
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
spinner.succeed(chalk.green(`Exported: ${result.outputPath}`));
|
|
969
|
-
outputResult({
|
|
970
|
-
success: true,
|
|
971
|
-
command: "export",
|
|
972
|
-
result: {
|
|
973
|
-
outputPath: result.outputPath,
|
|
974
|
-
backend: "hyperframes",
|
|
975
|
-
durationMs: result.durationMs,
|
|
976
|
-
framesRendered: result.framesRendered,
|
|
977
|
-
},
|
|
978
|
-
});
|
|
979
|
-
}
|
|
980
|
-
//# sourceMappingURL=export.js.map
|