@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,1014 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module edit-cmd
|
|
3
|
+
*
|
|
4
|
+
* Top-level `vibe edit` command group for post-production editing.
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* edit silence-cut - Remove silent segments (FFmpeg / Gemini)
|
|
8
|
+
* edit jump-cut - Remove filler words (Whisper + FFmpeg)
|
|
9
|
+
* edit caption - Add styled captions (Whisper + FFmpeg)
|
|
10
|
+
* edit noise-reduce - Audio/video noise removal (FFmpeg)
|
|
11
|
+
* edit fade - Fade in/out effects (FFmpeg)
|
|
12
|
+
* edit translate-srt - Translate SRT subtitles (Claude/OpenAI)
|
|
13
|
+
* edit grade - Color grading (Claude + FFmpeg)
|
|
14
|
+
* edit text-overlay - Text overlays (FFmpeg drawtext)
|
|
15
|
+
* edit speed-ramp - Speed ramping (Whisper + Claude + FFmpeg)
|
|
16
|
+
* edit reframe - Reframe aspect ratio (Claude Vision + FFmpeg)
|
|
17
|
+
* edit image - Image editing (Gemini/OpenAI/Grok)
|
|
18
|
+
* edit interpolate - Frame interpolation / slow motion (FFmpeg)
|
|
19
|
+
* edit upscale-video - Video upscaling (FFmpeg / Replicate)
|
|
20
|
+
* edit fill-gaps - Fill timeline gaps with AI video (Kling)
|
|
21
|
+
*
|
|
22
|
+
* @dependencies Whisper, Claude, Gemini, Kling, Replicate, FFmpeg
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { Command } from "commander";
|
|
26
|
+
import { resolve, dirname } from "node:path";
|
|
27
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
28
|
+
import chalk from "chalk";
|
|
29
|
+
import ora from "ora";
|
|
30
|
+
import {
|
|
31
|
+
WhisperProvider,
|
|
32
|
+
ClaudeProvider,
|
|
33
|
+
GeminiProvider,
|
|
34
|
+
OpenAIImageProvider,
|
|
35
|
+
GrokProvider,
|
|
36
|
+
} from "@vibeframe/ai-providers";
|
|
37
|
+
import { requireApiKey } from "../utils/api-key.js";
|
|
38
|
+
import { execSafe, commandExists } from "../utils/exec-safe.js";
|
|
39
|
+
import { formatTime } from "./ai-helpers.js";
|
|
40
|
+
import { applyTextOverlays, type TextOverlayStyle } from "./ai-edit.js";
|
|
41
|
+
import { registerEditCommands } from "./ai-edit-cli.js";
|
|
42
|
+
import { registerFillGapsCommand } from "./ai-fill-gaps.js";
|
|
43
|
+
import { isJsonMode, outputResult, exitWithError, usageError, notFoundError, apiError } from "./output.js";
|
|
44
|
+
import { rejectControlChars } from "./validate.js";
|
|
45
|
+
|
|
46
|
+
export const editCommand = new Command("edit")
|
|
47
|
+
.alias("ed")
|
|
48
|
+
.description(
|
|
49
|
+
"Edit and post-process media (silence-cut, caption, grade, reframe, upscale...)"
|
|
50
|
+
)
|
|
51
|
+
.addHelpText(
|
|
52
|
+
"after",
|
|
53
|
+
`
|
|
54
|
+
Examples:
|
|
55
|
+
$ vibe edit silence-cut interview.mp4 -o clean.mp4
|
|
56
|
+
$ vibe edit caption video.mp4 -o captioned.mp4 -s bold
|
|
57
|
+
$ vibe edit grade video.mp4 -o graded.mp4 --preset cinematic-warm
|
|
58
|
+
$ vibe edit reframe landscape.mp4 -o vertical.mp4 -a 9:16
|
|
59
|
+
$ vibe edit image photo.png "add sunset background" -o edited.png
|
|
60
|
+
$ vibe edit text-overlay video.mp4 -t "Title" -s center-bold -o out.mp4
|
|
61
|
+
$ vibe edit noise-reduce noisy.mp4 -o clean.mp4 -s high
|
|
62
|
+
$ vibe edit fade video.mp4 -o faded.mp4 --fade-in 1 --fade-out 1
|
|
63
|
+
|
|
64
|
+
API Keys (varies by subcommand):
|
|
65
|
+
No key needed silence-cut, noise-reduce, fade, interpolate, text-overlay
|
|
66
|
+
OPENAI_API_KEY caption, jump-cut (Whisper transcription)
|
|
67
|
+
ANTHROPIC_API_KEY grade, speed-ramp, reframe (Claude analysis)
|
|
68
|
+
GOOGLE_API_KEY image editing (Gemini, default)
|
|
69
|
+
|
|
70
|
+
Run 'vibe schema edit.<command>' for structured parameter info.
|
|
71
|
+
`
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// ── edit silence-cut, jump-cut, caption, noise-reduce, fade, translate-srt ──
|
|
75
|
+
|
|
76
|
+
registerEditCommands(editCommand);
|
|
77
|
+
|
|
78
|
+
// ── edit fill-gaps ──────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
registerFillGapsCommand(editCommand);
|
|
81
|
+
|
|
82
|
+
// ── edit grade ──────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
editCommand
|
|
85
|
+
.command("grade")
|
|
86
|
+
.description("Apply AI-generated color grading (Claude + FFmpeg)")
|
|
87
|
+
.argument("<video>", "Video file path")
|
|
88
|
+
.option("-s, --style <prompt>", "Style description (e.g., 'cinematic warm')")
|
|
89
|
+
.option("--preset <name>", "Built-in preset: film-noir, vintage, cinematic-warm, cool-tones, high-contrast, pastel, cyberpunk, horror")
|
|
90
|
+
.option("-o, --output <path>", "Output video file path")
|
|
91
|
+
.option("--analyze-only", "Show filter without applying")
|
|
92
|
+
.option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)")
|
|
93
|
+
.option("--dry-run", "Preview parameters without executing")
|
|
94
|
+
.action(async (videoPath: string, options) => {
|
|
95
|
+
try {
|
|
96
|
+
if (options.style) rejectControlChars(options.style);
|
|
97
|
+
|
|
98
|
+
if (!options.style && !options.preset) {
|
|
99
|
+
exitWithError(usageError(
|
|
100
|
+
"Either --style or --preset is required",
|
|
101
|
+
'Examples: vibe edit grade video.mp4 --style "warm sunset" or --preset cinematic-warm',
|
|
102
|
+
));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check FFmpeg
|
|
106
|
+
if (!commandExists("ffmpeg")) {
|
|
107
|
+
exitWithError(notFoundError("FFmpeg not found. Install with: brew install ffmpeg"));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (options.dryRun) {
|
|
111
|
+
outputResult({
|
|
112
|
+
dryRun: true,
|
|
113
|
+
command: "edit grade",
|
|
114
|
+
params: {
|
|
115
|
+
videoPath: resolve(process.cwd(), videoPath),
|
|
116
|
+
style: options.style || options.preset,
|
|
117
|
+
analyzeOnly: options.analyzeOnly || false,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const spinner = ora("Analyzing color grade...").start();
|
|
124
|
+
|
|
125
|
+
// Get API key if using style (not preset)
|
|
126
|
+
let gradeResult: { ffmpegFilter: string; description: string };
|
|
127
|
+
|
|
128
|
+
if (options.preset) {
|
|
129
|
+
const claude = new ClaudeProvider();
|
|
130
|
+
gradeResult = await claude.analyzeColorGrade("", options.preset);
|
|
131
|
+
} else {
|
|
132
|
+
let apiKey: string;
|
|
133
|
+
try {
|
|
134
|
+
apiKey = await requireApiKey("ANTHROPIC_API_KEY", "Anthropic", options.apiKey);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
spinner.fail((err as Error).message);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const claude = new ClaudeProvider();
|
|
140
|
+
await claude.initialize({ apiKey });
|
|
141
|
+
gradeResult = await claude.analyzeColorGrade(options.style);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
spinner.succeed(chalk.green("Color grade analyzed"));
|
|
145
|
+
|
|
146
|
+
if (isJsonMode()) {
|
|
147
|
+
const absPath = resolve(process.cwd(), videoPath);
|
|
148
|
+
const gradeOutputPath = options.output
|
|
149
|
+
? resolve(process.cwd(), options.output)
|
|
150
|
+
: absPath.replace(/(\.[^.]+)$/, "-graded$1");
|
|
151
|
+
outputResult({
|
|
152
|
+
success: true,
|
|
153
|
+
style: options.preset || options.style,
|
|
154
|
+
description: gradeResult.description,
|
|
155
|
+
ffmpegFilter: gradeResult.ffmpegFilter,
|
|
156
|
+
outputPath: options.analyzeOnly ? undefined : gradeOutputPath,
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log();
|
|
162
|
+
console.log(chalk.bold.cyan("Color Grade"));
|
|
163
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
164
|
+
console.log(`Style: ${options.preset || options.style}`);
|
|
165
|
+
console.log(`Description: ${gradeResult.description}`);
|
|
166
|
+
console.log();
|
|
167
|
+
console.log(chalk.dim("FFmpeg filter:"));
|
|
168
|
+
console.log(chalk.cyan(gradeResult.ffmpegFilter));
|
|
169
|
+
console.log();
|
|
170
|
+
|
|
171
|
+
if (options.analyzeOnly) {
|
|
172
|
+
console.log(chalk.dim("Use without --analyze-only to apply the grade."));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const absPath = resolve(process.cwd(), videoPath);
|
|
177
|
+
const outputPath = options.output
|
|
178
|
+
? resolve(process.cwd(), options.output)
|
|
179
|
+
: absPath.replace(/(\.[^.]+)$/, "-graded$1");
|
|
180
|
+
|
|
181
|
+
spinner.start("Applying color grade...");
|
|
182
|
+
|
|
183
|
+
await execSafe("ffmpeg", ["-i", absPath, "-vf", gradeResult.ffmpegFilter, "-c:a", "copy", outputPath, "-y"], { timeout: 600000 });
|
|
184
|
+
|
|
185
|
+
spinner.succeed(chalk.green("Color grade applied"));
|
|
186
|
+
console.log(chalk.green(`Output: ${outputPath}`));
|
|
187
|
+
console.log();
|
|
188
|
+
} catch (error) {
|
|
189
|
+
exitWithError(apiError(`Color grading failed: ${(error as Error).message}`));
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ── edit text-overlay ───────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
editCommand
|
|
196
|
+
.command("text-overlay")
|
|
197
|
+
.description("Apply text overlays to video (FFmpeg drawtext)")
|
|
198
|
+
.argument("<video>", "Video file path")
|
|
199
|
+
.option("-t, --text <texts...>", "Text lines to overlay (repeat for multiple)")
|
|
200
|
+
.option("-s, --style <style>", "Overlay style: lower-third, center-bold, subtitle, minimal", "lower-third")
|
|
201
|
+
.option("--font-size <size>", "Font size in pixels (auto-calculated if omitted)")
|
|
202
|
+
.option("--font-color <color>", "Font color (default: white)", "white")
|
|
203
|
+
.option("--fade <seconds>", "Fade in/out duration in seconds", "0.3")
|
|
204
|
+
.option("--start <seconds>", "Start time in seconds", "0")
|
|
205
|
+
.option("--end <seconds>", "End time in seconds (default: video duration)")
|
|
206
|
+
.option("-o, --output <path>", "Output video file path")
|
|
207
|
+
.option("--dry-run", "Preview parameters without executing")
|
|
208
|
+
.action(async (videoPath: string, options) => {
|
|
209
|
+
try {
|
|
210
|
+
if (!options.text || options.text.length === 0) {
|
|
211
|
+
console.error(chalk.red("At least one --text option is required"));
|
|
212
|
+
console.log(chalk.dim("Example:"));
|
|
213
|
+
console.log(chalk.dim(' pnpm vibe edit text-overlay video.mp4 -t "NEXUS AI" -t "Intelligence, Unleashed" --style center-bold'));
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const t of options.text) rejectControlChars(t);
|
|
218
|
+
|
|
219
|
+
// Check FFmpeg
|
|
220
|
+
if (!commandExists("ffmpeg")) {
|
|
221
|
+
console.error(chalk.red("FFmpeg not found. Please install FFmpeg."));
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (options.dryRun) {
|
|
226
|
+
outputResult({
|
|
227
|
+
dryRun: true,
|
|
228
|
+
command: "edit text-overlay",
|
|
229
|
+
params: {
|
|
230
|
+
videoPath: resolve(process.cwd(), videoPath),
|
|
231
|
+
texts: options.text,
|
|
232
|
+
style: options.style,
|
|
233
|
+
fontSize: options.fontSize ? parseInt(options.fontSize) : undefined,
|
|
234
|
+
fontColor: options.fontColor,
|
|
235
|
+
fade: parseFloat(options.fade),
|
|
236
|
+
start: parseFloat(options.start),
|
|
237
|
+
end: options.end ? parseFloat(options.end) : undefined,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const absPath = resolve(process.cwd(), videoPath);
|
|
244
|
+
const outputPath = options.output
|
|
245
|
+
? resolve(process.cwd(), options.output)
|
|
246
|
+
: absPath.replace(/(\.[^.]+)$/, "-overlay$1");
|
|
247
|
+
|
|
248
|
+
const spinner = ora("Applying text overlays...").start();
|
|
249
|
+
|
|
250
|
+
const result = await applyTextOverlays({
|
|
251
|
+
videoPath: absPath,
|
|
252
|
+
texts: options.text,
|
|
253
|
+
outputPath,
|
|
254
|
+
style: options.style as TextOverlayStyle,
|
|
255
|
+
fontSize: options.fontSize ? parseInt(options.fontSize) : undefined,
|
|
256
|
+
fontColor: options.fontColor,
|
|
257
|
+
fadeDuration: parseFloat(options.fade),
|
|
258
|
+
startTime: parseFloat(options.start),
|
|
259
|
+
endTime: options.end ? parseFloat(options.end) : undefined,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!result.success) {
|
|
263
|
+
spinner.fail(chalk.red(result.error || "Text overlay failed"));
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
spinner.succeed(chalk.green("Text overlays applied"));
|
|
268
|
+
|
|
269
|
+
if (isJsonMode()) {
|
|
270
|
+
outputResult({
|
|
271
|
+
success: true,
|
|
272
|
+
style: options.style,
|
|
273
|
+
texts: options.text,
|
|
274
|
+
outputPath: result.outputPath,
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log();
|
|
280
|
+
console.log(chalk.bold.cyan("Text Overlay"));
|
|
281
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
282
|
+
console.log(`Style: ${options.style}`);
|
|
283
|
+
console.log(`Texts: ${options.text.join(", ")}`);
|
|
284
|
+
console.log(`Output: ${result.outputPath}`);
|
|
285
|
+
console.log();
|
|
286
|
+
} catch (error) {
|
|
287
|
+
console.error(chalk.red("Text overlay failed"));
|
|
288
|
+
console.error(error);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// ── edit speed-ramp ─────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
editCommand
|
|
296
|
+
.command("speed-ramp")
|
|
297
|
+
.description("Apply content-aware speed ramping (Whisper + Claude + FFmpeg)")
|
|
298
|
+
.argument("<video>", "Video file path")
|
|
299
|
+
.option("-o, --output <path>", "Output video file path")
|
|
300
|
+
.option("-s, --style <style>", "Style: dramatic, smooth, action", "dramatic")
|
|
301
|
+
.option("--min-speed <factor>", "Minimum speed factor", "0.25")
|
|
302
|
+
.option("--max-speed <factor>", "Maximum speed factor", "4.0")
|
|
303
|
+
.option("--analyze-only", "Show keyframes without applying")
|
|
304
|
+
.option("-l, --language <lang>", "Language code for transcription")
|
|
305
|
+
.option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)")
|
|
306
|
+
.option("--dry-run", "Preview parameters without executing")
|
|
307
|
+
.action(async (videoPath: string, options) => {
|
|
308
|
+
try {
|
|
309
|
+
// Check FFmpeg
|
|
310
|
+
if (!commandExists("ffmpeg")) {
|
|
311
|
+
console.error(chalk.red("FFmpeg not found. Please install FFmpeg."));
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (options.dryRun) {
|
|
316
|
+
outputResult({
|
|
317
|
+
dryRun: true,
|
|
318
|
+
command: "edit speed-ramp",
|
|
319
|
+
params: {
|
|
320
|
+
videoPath: resolve(process.cwd(), videoPath),
|
|
321
|
+
style: options.style,
|
|
322
|
+
minSpeed: parseFloat(options.minSpeed),
|
|
323
|
+
maxSpeed: parseFloat(options.maxSpeed),
|
|
324
|
+
analyzeOnly: options.analyzeOnly || false,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const openaiApiKey = await requireApiKey("OPENAI_API_KEY", "OpenAI");
|
|
331
|
+
const claudeApiKey = await requireApiKey("ANTHROPIC_API_KEY", "Anthropic", options.apiKey);
|
|
332
|
+
|
|
333
|
+
const absPath = resolve(process.cwd(), videoPath);
|
|
334
|
+
|
|
335
|
+
// Step 1: Check for audio stream
|
|
336
|
+
const spinner = ora("Extracting audio...").start();
|
|
337
|
+
|
|
338
|
+
const { stdout: speedRampProbe } = await execSafe("ffprobe", [
|
|
339
|
+
"-v", "error", "-select_streams", "a", "-show_entries", "stream=codec_type", "-of", "csv=p=0", absPath,
|
|
340
|
+
]);
|
|
341
|
+
if (!speedRampProbe.trim()) {
|
|
342
|
+
spinner.fail(chalk.yellow("Video has no audio track — cannot use Whisper transcription"));
|
|
343
|
+
console.log(chalk.yellow("\nThis video has no audio stream."));
|
|
344
|
+
console.log(chalk.dim(" Speed ramping requires audio for content-aware analysis."));
|
|
345
|
+
console.log(chalk.dim(" Please use a video with an audio track.\n"));
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const tempAudio = absPath.replace(/(\.[^.]+)$/, "-temp-audio.mp3");
|
|
350
|
+
|
|
351
|
+
await execSafe("ffmpeg", ["-i", absPath, "-vn", "-acodec", "libmp3lame", "-q:a", "2", tempAudio, "-y"]);
|
|
352
|
+
|
|
353
|
+
// Step 2: Transcribe
|
|
354
|
+
spinner.text = "Transcribing audio...";
|
|
355
|
+
|
|
356
|
+
const whisper = new WhisperProvider();
|
|
357
|
+
await whisper.initialize({ apiKey: openaiApiKey });
|
|
358
|
+
|
|
359
|
+
const audioBuffer = await readFile(tempAudio);
|
|
360
|
+
const audioBlob = new Blob([audioBuffer]);
|
|
361
|
+
const transcript = await whisper.transcribe(audioBlob, options.language);
|
|
362
|
+
|
|
363
|
+
if (!transcript.segments || transcript.segments.length === 0) {
|
|
364
|
+
spinner.fail(chalk.red("No transcript segments found"));
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Step 3: Analyze with Claude
|
|
369
|
+
spinner.text = "Analyzing for speed ramping...";
|
|
370
|
+
|
|
371
|
+
const claude = new ClaudeProvider();
|
|
372
|
+
await claude.initialize({ apiKey: claudeApiKey });
|
|
373
|
+
|
|
374
|
+
const speedResult = await claude.analyzeForSpeedRamp(transcript.segments, {
|
|
375
|
+
style: options.style as "dramatic" | "smooth" | "action",
|
|
376
|
+
minSpeed: parseFloat(options.minSpeed),
|
|
377
|
+
maxSpeed: parseFloat(options.maxSpeed),
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Clean up temp file
|
|
381
|
+
try {
|
|
382
|
+
const { unlink } = await import("node:fs/promises");
|
|
383
|
+
await unlink(tempAudio);
|
|
384
|
+
} catch { /* ignore cleanup errors */ }
|
|
385
|
+
|
|
386
|
+
spinner.succeed(chalk.green(`Found ${speedResult.keyframes.length} speed keyframes`));
|
|
387
|
+
|
|
388
|
+
if (isJsonMode()) {
|
|
389
|
+
const avgSpeed = speedResult.keyframes.reduce((sum, kf) => sum + kf.speed, 0) / speedResult.keyframes.length;
|
|
390
|
+
const speedRampOutputPath = options.output
|
|
391
|
+
? resolve(process.cwd(), options.output)
|
|
392
|
+
: absPath.replace(/(\.[^.]+)$/, "-ramped$1");
|
|
393
|
+
outputResult({
|
|
394
|
+
success: true,
|
|
395
|
+
keyframes: speedResult.keyframes,
|
|
396
|
+
avgSpeed,
|
|
397
|
+
outputPath: options.analyzeOnly ? undefined : speedRampOutputPath,
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
console.log();
|
|
403
|
+
console.log(chalk.bold.cyan("Speed Ramp Keyframes"));
|
|
404
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
405
|
+
|
|
406
|
+
for (const kf of speedResult.keyframes) {
|
|
407
|
+
const speedColor = kf.speed < 1 ? chalk.blue : kf.speed > 1 ? chalk.yellow : chalk.white;
|
|
408
|
+
console.log(` ${formatTime(kf.time)} → ${speedColor(`${kf.speed.toFixed(2)}x`)} - ${kf.reason}`);
|
|
409
|
+
}
|
|
410
|
+
console.log();
|
|
411
|
+
|
|
412
|
+
if (options.analyzeOnly) {
|
|
413
|
+
console.log(chalk.dim("Use without --analyze-only to apply speed ramps."));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (speedResult.keyframes.length < 2) {
|
|
418
|
+
console.log(chalk.yellow("Not enough keyframes for speed ramping."));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
spinner.start("Applying speed ramps...");
|
|
423
|
+
|
|
424
|
+
// Build FFmpeg filter for speed ramping (segment-based)
|
|
425
|
+
const outputPath = options.output
|
|
426
|
+
? resolve(process.cwd(), options.output)
|
|
427
|
+
: absPath.replace(/(\.[^.]+)$/, "-ramped$1");
|
|
428
|
+
|
|
429
|
+
// For simplicity, we'll create segments and concatenate
|
|
430
|
+
// A full implementation would use complex filter expressions
|
|
431
|
+
// Here we use setpts with a simple approach
|
|
432
|
+
|
|
433
|
+
// For demo, apply average speed or first segment's speed
|
|
434
|
+
const avgSpeed = speedResult.keyframes.reduce((sum, kf) => sum + kf.speed, 0) / speedResult.keyframes.length;
|
|
435
|
+
|
|
436
|
+
// Use setpts for speed change (1/speed for setpts)
|
|
437
|
+
const setpts = `setpts=${(1 / avgSpeed).toFixed(3)}*PTS`;
|
|
438
|
+
const atempo = avgSpeed >= 0.5 && avgSpeed <= 2.0 ? `atempo=${avgSpeed.toFixed(3)}` : "";
|
|
439
|
+
|
|
440
|
+
if (atempo) {
|
|
441
|
+
await execSafe("ffmpeg", ["-i", absPath, "-filter_complex", `[0:v]${setpts}[v];[0:a]${atempo}[a]`, "-map", "[v]", "-map", "[a]", outputPath, "-y"], { timeout: 600000 });
|
|
442
|
+
} else {
|
|
443
|
+
await execSafe("ffmpeg", ["-i", absPath, "-vf", setpts, "-an", outputPath, "-y"], { timeout: 600000 });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
spinner.succeed(chalk.green("Speed ramp applied"));
|
|
447
|
+
console.log(chalk.green(`Output: ${outputPath}`));
|
|
448
|
+
console.log(chalk.dim(`Average speed: ${avgSpeed.toFixed(2)}x`));
|
|
449
|
+
console.log();
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.error(chalk.red("Speed ramping failed"));
|
|
452
|
+
console.error(error);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// ── edit reframe ────────────────────────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
editCommand
|
|
460
|
+
.command("reframe")
|
|
461
|
+
.description("Auto-reframe video to different aspect ratio (Claude Vision + FFmpeg)")
|
|
462
|
+
.argument("<video>", "Video file path")
|
|
463
|
+
.option("-a, --aspect <ratio>", "Target aspect ratio: 9:16, 1:1, 4:5", "9:16")
|
|
464
|
+
.option("-f, --focus <mode>", "Focus mode: auto, face, center, action", "auto")
|
|
465
|
+
.option("-o, --output <path>", "Output video file path")
|
|
466
|
+
.option("--analyze-only", "Show crop regions without applying")
|
|
467
|
+
.option("--keyframes <path>", "Export keyframes to JSON file")
|
|
468
|
+
.option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)")
|
|
469
|
+
.option("--dry-run", "Preview parameters without executing")
|
|
470
|
+
.action(async (videoPath: string, options) => {
|
|
471
|
+
try {
|
|
472
|
+
// Check FFmpeg
|
|
473
|
+
if (!commandExists("ffmpeg")) {
|
|
474
|
+
console.error(chalk.red("FFmpeg not found. Please install FFmpeg."));
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (options.dryRun) {
|
|
479
|
+
outputResult({
|
|
480
|
+
dryRun: true,
|
|
481
|
+
command: "edit reframe",
|
|
482
|
+
params: {
|
|
483
|
+
videoPath: resolve(process.cwd(), videoPath),
|
|
484
|
+
aspect: options.aspect,
|
|
485
|
+
focus: options.focus,
|
|
486
|
+
analyzeOnly: options.analyzeOnly || false,
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const absPath = resolve(process.cwd(), videoPath);
|
|
493
|
+
|
|
494
|
+
// Get video dimensions
|
|
495
|
+
const spinner = ora("Analyzing video...").start();
|
|
496
|
+
|
|
497
|
+
const { stdout: probeOut } = await execSafe("ffprobe", [
|
|
498
|
+
"-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,duration", "-of", "csv=p=0", absPath,
|
|
499
|
+
]);
|
|
500
|
+
const [width, height, durationStr] = probeOut.trim().split(",");
|
|
501
|
+
const sourceWidth = parseInt(width);
|
|
502
|
+
const sourceHeight = parseInt(height);
|
|
503
|
+
const duration = parseFloat(durationStr);
|
|
504
|
+
|
|
505
|
+
spinner.text = "Extracting keyframes...";
|
|
506
|
+
|
|
507
|
+
// Extract keyframes every 2 seconds for analysis
|
|
508
|
+
const keyframeInterval = 2;
|
|
509
|
+
const numKeyframes = Math.ceil(duration / keyframeInterval);
|
|
510
|
+
const tempDir = `/tmp/vibe-reframe-${Date.now()}`;
|
|
511
|
+
const { mkdir: mkdirFs } = await import("node:fs/promises");
|
|
512
|
+
await mkdirFs(tempDir, { recursive: true });
|
|
513
|
+
|
|
514
|
+
await execSafe("ffmpeg", ["-i", absPath, "-vf", `fps=1/${keyframeInterval}`, "-frame_pts", "1", `${tempDir}/frame-%04d.jpg`, "-y"]);
|
|
515
|
+
|
|
516
|
+
// Get API key
|
|
517
|
+
let apiKey: string;
|
|
518
|
+
try {
|
|
519
|
+
apiKey = await requireApiKey("ANTHROPIC_API_KEY", "Anthropic", options.apiKey);
|
|
520
|
+
} catch (err) {
|
|
521
|
+
spinner.fail((err as Error).message);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const claude = new ClaudeProvider();
|
|
525
|
+
await claude.initialize({ apiKey });
|
|
526
|
+
|
|
527
|
+
// Analyze keyframes
|
|
528
|
+
spinner.text = "Analyzing frames for subject tracking...";
|
|
529
|
+
|
|
530
|
+
const cropKeyframes: Array<{
|
|
531
|
+
time: number;
|
|
532
|
+
cropX: number;
|
|
533
|
+
cropY: number;
|
|
534
|
+
cropWidth: number;
|
|
535
|
+
cropHeight: number;
|
|
536
|
+
confidence: number;
|
|
537
|
+
subjectDescription: string;
|
|
538
|
+
}> = [];
|
|
539
|
+
|
|
540
|
+
for (let i = 1; i <= numKeyframes && i <= 30; i++) {
|
|
541
|
+
// Limit to 30 frames
|
|
542
|
+
const framePath = `${tempDir}/frame-${i.toString().padStart(4, "0")}.jpg`;
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
const frameBuffer = await readFile(framePath);
|
|
546
|
+
const frameBase64 = frameBuffer.toString("base64");
|
|
547
|
+
|
|
548
|
+
const result = await claude.analyzeFrameForReframe(frameBase64, options.aspect, {
|
|
549
|
+
focusMode: options.focus,
|
|
550
|
+
sourceWidth,
|
|
551
|
+
sourceHeight,
|
|
552
|
+
mimeType: "image/jpeg",
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
cropKeyframes.push({
|
|
556
|
+
time: (i - 1) * keyframeInterval,
|
|
557
|
+
...result,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
spinner.text = `Analyzing frames... ${i}/${Math.min(numKeyframes, 30)}`;
|
|
561
|
+
} catch {
|
|
562
|
+
// Skip failed frames
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Small delay to avoid rate limiting
|
|
566
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Clean up temp files
|
|
570
|
+
try {
|
|
571
|
+
const { rm: rmFs } = await import("node:fs/promises");
|
|
572
|
+
await rmFs(tempDir, { recursive: true, force: true });
|
|
573
|
+
} catch { /* ignore cleanup errors */ }
|
|
574
|
+
|
|
575
|
+
spinner.succeed(chalk.green(`Analyzed ${cropKeyframes.length} keyframes`));
|
|
576
|
+
|
|
577
|
+
if (isJsonMode()) {
|
|
578
|
+
const reframeOutputPath = options.output
|
|
579
|
+
? resolve(process.cwd(), options.output)
|
|
580
|
+
: absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
|
|
581
|
+
outputResult({
|
|
582
|
+
success: true,
|
|
583
|
+
sourceWidth,
|
|
584
|
+
sourceHeight,
|
|
585
|
+
aspect: options.aspect,
|
|
586
|
+
cropKeyframes,
|
|
587
|
+
outputPath: options.analyzeOnly ? undefined : reframeOutputPath,
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
console.log();
|
|
593
|
+
console.log(chalk.bold.cyan("Reframe Analysis"));
|
|
594
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
595
|
+
console.log(`Source: ${sourceWidth}x${sourceHeight}`);
|
|
596
|
+
console.log(`Target: ${options.aspect}`);
|
|
597
|
+
console.log(`Focus: ${options.focus}`);
|
|
598
|
+
console.log();
|
|
599
|
+
|
|
600
|
+
if (cropKeyframes.length > 0) {
|
|
601
|
+
const avgConf = cropKeyframes.reduce((sum, kf) => sum + kf.confidence, 0) / cropKeyframes.length;
|
|
602
|
+
console.log(`Average confidence: ${(avgConf * 100).toFixed(0)}%`);
|
|
603
|
+
console.log();
|
|
604
|
+
console.log(chalk.dim("Sample keyframes:"));
|
|
605
|
+
for (const kf of cropKeyframes.slice(0, 5)) {
|
|
606
|
+
console.log(` ${formatTime(kf.time)} → crop=${kf.cropX},${kf.cropY} (${kf.subjectDescription})`);
|
|
607
|
+
}
|
|
608
|
+
if (cropKeyframes.length > 5) {
|
|
609
|
+
console.log(chalk.dim(` ... and ${cropKeyframes.length - 5} more`));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
console.log();
|
|
613
|
+
|
|
614
|
+
// Export keyframes if requested
|
|
615
|
+
if (options.keyframes) {
|
|
616
|
+
const keyframesPath = resolve(process.cwd(), options.keyframes);
|
|
617
|
+
await writeFile(keyframesPath, JSON.stringify(cropKeyframes, null, 2));
|
|
618
|
+
console.log(chalk.green(`Keyframes saved to: ${keyframesPath}`));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (options.analyzeOnly) {
|
|
622
|
+
console.log(chalk.dim("Use without --analyze-only to apply reframe."));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Apply reframe using average crop position
|
|
627
|
+
const avgCropX = Math.round(cropKeyframes.reduce((sum, kf) => sum + kf.cropX, 0) / cropKeyframes.length);
|
|
628
|
+
const avgCropY = Math.round(cropKeyframes.reduce((sum, kf) => sum + kf.cropY, 0) / cropKeyframes.length);
|
|
629
|
+
const cropWidth = cropKeyframes[0]?.cropWidth || sourceWidth;
|
|
630
|
+
const cropHeight = cropKeyframes[0]?.cropHeight || sourceHeight;
|
|
631
|
+
|
|
632
|
+
const outputPath = options.output
|
|
633
|
+
? resolve(process.cwd(), options.output)
|
|
634
|
+
: absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
|
|
635
|
+
|
|
636
|
+
spinner.start("Applying reframe...");
|
|
637
|
+
|
|
638
|
+
await execSafe("ffmpeg", ["-i", absPath, "-vf", `crop=${cropWidth}:${cropHeight}:${avgCropX}:${avgCropY}`, "-c:a", "copy", outputPath, "-y"], { timeout: 600000 });
|
|
639
|
+
|
|
640
|
+
spinner.succeed(chalk.green("Reframe applied"));
|
|
641
|
+
console.log(chalk.green(`Output: ${outputPath}`));
|
|
642
|
+
console.log(chalk.dim(`Crop: ${cropWidth}x${cropHeight} at (${avgCropX}, ${avgCropY})`));
|
|
643
|
+
console.log();
|
|
644
|
+
} catch (error) {
|
|
645
|
+
console.error(chalk.red("Reframe failed"));
|
|
646
|
+
console.error(error);
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// ── edit image (Gemini multi-image editing) ─────────────────────────────
|
|
652
|
+
|
|
653
|
+
editCommand
|
|
654
|
+
.command("image")
|
|
655
|
+
.description("Edit image(s) using AI (Gemini/OpenAI/Grok)")
|
|
656
|
+
.argument("<images...>", "Input image file(s) followed by edit prompt")
|
|
657
|
+
.option("-p, --provider <provider>", "Provider: gemini (default), openai, grok", "gemini")
|
|
658
|
+
.option("-k, --api-key <key>", "API key (or set env variable)")
|
|
659
|
+
.option("-o, --output <path>", "Output file path", "edited.png")
|
|
660
|
+
.option("-m, --model <model>", "Model: flash/3.1-flash/latest/pro (Gemini only)", "flash")
|
|
661
|
+
.option("-r, --ratio <ratio>", "Output aspect ratio")
|
|
662
|
+
.option("-s, --size <resolution>", "Resolution: 1K, 2K, 4K (Gemini Pro only)")
|
|
663
|
+
.option("--dry-run", "Preview parameters without executing")
|
|
664
|
+
.action(async (args: string[], options) => {
|
|
665
|
+
try {
|
|
666
|
+
// Last argument is the prompt, rest are image paths
|
|
667
|
+
if (args.length < 2) {
|
|
668
|
+
console.error(chalk.red("Need at least one image and a prompt"));
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const prompt = args[args.length - 1];
|
|
673
|
+
rejectControlChars(prompt);
|
|
674
|
+
const imagePaths = args.slice(0, -1);
|
|
675
|
+
const provider = options.provider as string;
|
|
676
|
+
|
|
677
|
+
// Grok only supports 1 image
|
|
678
|
+
if (provider === "grok" && imagePaths.length > 1) {
|
|
679
|
+
console.error(chalk.red("Grok supports only 1 input image for editing."));
|
|
680
|
+
console.log(chalk.dim("Use -p gemini (up to 14 images) or -p openai (up to 16 images) for multi-image editing."));
|
|
681
|
+
process.exit(1);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (options.dryRun) {
|
|
685
|
+
outputResult({
|
|
686
|
+
dryRun: true,
|
|
687
|
+
command: "edit image",
|
|
688
|
+
params: {
|
|
689
|
+
imagePaths: imagePaths.map((p: string) => resolve(process.cwd(), p)),
|
|
690
|
+
prompt,
|
|
691
|
+
provider,
|
|
692
|
+
model: options.model,
|
|
693
|
+
ratio: options.ratio,
|
|
694
|
+
size: options.size,
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Provider-specific API key resolution
|
|
701
|
+
const apiKeyMap: Record<string, { envVar: string; label: string }> = {
|
|
702
|
+
gemini: { envVar: "GOOGLE_API_KEY", label: "Google" },
|
|
703
|
+
openai: { envVar: "OPENAI_API_KEY", label: "OpenAI" },
|
|
704
|
+
grok: { envVar: "XAI_API_KEY", label: "xAI" },
|
|
705
|
+
};
|
|
706
|
+
const keyInfo = apiKeyMap[provider] || apiKeyMap.gemini;
|
|
707
|
+
const apiKey = await requireApiKey(keyInfo.envVar, keyInfo.label, options.apiKey);
|
|
708
|
+
|
|
709
|
+
const spinner = ora(`Reading ${imagePaths.length} image(s)...`).start();
|
|
710
|
+
|
|
711
|
+
// Load all images
|
|
712
|
+
const imageBuffers: Buffer[] = [];
|
|
713
|
+
for (const imagePath of imagePaths) {
|
|
714
|
+
const absPath = resolve(process.cwd(), imagePath);
|
|
715
|
+
const buffer = await readFile(absPath);
|
|
716
|
+
imageBuffers.push(buffer);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
let result: import("@vibeframe/ai-providers").ImageResult;
|
|
720
|
+
|
|
721
|
+
if (provider === "openai") {
|
|
722
|
+
spinner.text = "Editing with GPT Image 1.5...";
|
|
723
|
+
const openaiImage = new OpenAIImageProvider();
|
|
724
|
+
await openaiImage.initialize({ apiKey });
|
|
725
|
+
result = await openaiImage.editImage(imageBuffers, prompt);
|
|
726
|
+
} else if (provider === "grok") {
|
|
727
|
+
spinner.text = "Editing with Grok Imagine...";
|
|
728
|
+
const grok = new GrokProvider();
|
|
729
|
+
await grok.initialize({ apiKey });
|
|
730
|
+
result = await grok.editImage(imageBuffers[0], prompt, {
|
|
731
|
+
aspectRatio: options.ratio,
|
|
732
|
+
});
|
|
733
|
+
} else {
|
|
734
|
+
// Gemini (default)
|
|
735
|
+
const editModelNames: Record<string, string> = {
|
|
736
|
+
flash: "gemini-2.5-flash-image",
|
|
737
|
+
"3.1-flash": "gemini-3.1-flash-image-preview",
|
|
738
|
+
latest: "gemini-3.1-flash-image-preview",
|
|
739
|
+
pro: "gemini-3-pro-image-preview",
|
|
740
|
+
};
|
|
741
|
+
const editModelName = editModelNames[options.model] || editModelNames.flash;
|
|
742
|
+
spinner.text = `Editing with ${editModelName}...`;
|
|
743
|
+
|
|
744
|
+
const gemini = new GeminiProvider();
|
|
745
|
+
await gemini.initialize({ apiKey });
|
|
746
|
+
|
|
747
|
+
result = await gemini.editImage(imageBuffers, prompt, {
|
|
748
|
+
model: options.model,
|
|
749
|
+
aspectRatio: options.ratio,
|
|
750
|
+
resolution: options.size,
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Auto-fallback: if latest/3.1-flash fails, retry with flash
|
|
754
|
+
const fallbackModels = ["latest", "3.1-flash"];
|
|
755
|
+
if (!result.success && fallbackModels.includes(options.model)) {
|
|
756
|
+
spinner.text = `${chalk.dim(result.error || `${editModelName} failed`)} — retrying with flash...`;
|
|
757
|
+
result = await gemini.editImage(imageBuffers, prompt, {
|
|
758
|
+
model: "flash",
|
|
759
|
+
aspectRatio: options.ratio,
|
|
760
|
+
resolution: options.size,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (!result.success || !result.images || result.images.length === 0) {
|
|
766
|
+
spinner.fail(chalk.red(result.error || "Image editing failed"));
|
|
767
|
+
process.exit(1);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
spinner.succeed(chalk.green("Image edited"));
|
|
771
|
+
|
|
772
|
+
// Save image — handle both base64 and URL responses
|
|
773
|
+
const img = result.images[0];
|
|
774
|
+
const outputPath = resolve(process.cwd(), options.output);
|
|
775
|
+
|
|
776
|
+
const saveImage = async () => {
|
|
777
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
778
|
+
if (img.base64) {
|
|
779
|
+
const buffer = Buffer.from(img.base64, "base64");
|
|
780
|
+
await writeFile(outputPath, buffer);
|
|
781
|
+
} else if (img.url) {
|
|
782
|
+
const resp = await fetch(img.url);
|
|
783
|
+
const arrayBuf = await resp.arrayBuffer();
|
|
784
|
+
await writeFile(outputPath, Buffer.from(arrayBuf));
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
// Gemini results may include a `model` field
|
|
789
|
+
const resultModel = (result as { model?: string }).model;
|
|
790
|
+
|
|
791
|
+
if (isJsonMode()) {
|
|
792
|
+
outputResult({
|
|
793
|
+
success: true,
|
|
794
|
+
provider,
|
|
795
|
+
model: resultModel || options.model,
|
|
796
|
+
outputPath,
|
|
797
|
+
});
|
|
798
|
+
await saveImage();
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (resultModel) {
|
|
803
|
+
console.log(chalk.dim(`Model: ${resultModel}`));
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
await saveImage();
|
|
807
|
+
console.log(chalk.green(`Saved to: ${outputPath}`));
|
|
808
|
+
} catch (error) {
|
|
809
|
+
console.error(chalk.red("Image editing failed"));
|
|
810
|
+
console.error(error);
|
|
811
|
+
process.exit(1);
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// ── edit interpolate (frame interpolation / slow motion) ────────────────
|
|
816
|
+
|
|
817
|
+
editCommand
|
|
818
|
+
.command("interpolate")
|
|
819
|
+
.description("Create slow motion with frame interpolation (FFmpeg)")
|
|
820
|
+
.argument("<video>", "Video file path")
|
|
821
|
+
.option("-o, --output <path>", "Output file path")
|
|
822
|
+
.option("-f, --factor <number>", "Slow motion factor: 2, 4, or 8", "2")
|
|
823
|
+
.option("--fps <number>", "Target output FPS")
|
|
824
|
+
.option("-q, --quality <mode>", "Quality: fast or quality", "quality")
|
|
825
|
+
.option("--dry-run", "Preview parameters without executing")
|
|
826
|
+
.action(async (videoPath: string, options) => {
|
|
827
|
+
try {
|
|
828
|
+
const absPath = resolve(process.cwd(), videoPath);
|
|
829
|
+
const factor = parseInt(options.factor);
|
|
830
|
+
|
|
831
|
+
if (![2, 4, 8].includes(factor)) {
|
|
832
|
+
console.error(chalk.red("Factor must be 2, 4, or 8"));
|
|
833
|
+
process.exit(1);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (options.dryRun) {
|
|
837
|
+
outputResult({
|
|
838
|
+
dryRun: true,
|
|
839
|
+
command: "edit interpolate",
|
|
840
|
+
params: {
|
|
841
|
+
videoPath: absPath,
|
|
842
|
+
factor,
|
|
843
|
+
fps: options.fps ? parseInt(options.fps) : undefined,
|
|
844
|
+
quality: options.quality,
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const outputPath = options.output
|
|
851
|
+
? resolve(process.cwd(), options.output)
|
|
852
|
+
: absPath.replace(/(\.[^.]+)$/, `-slow${factor}x$1`);
|
|
853
|
+
|
|
854
|
+
const spinner = ora(`Creating ${factor}x slow motion...`).start();
|
|
855
|
+
|
|
856
|
+
try {
|
|
857
|
+
// Get original FPS
|
|
858
|
+
const { stdout: fpsOut } = await execSafe("ffprobe", [
|
|
859
|
+
"-v", "error", "-select_streams", "v:0", "-show_entries", "stream=r_frame_rate", "-of", "default=noprint_wrappers=1:nokey=1", absPath,
|
|
860
|
+
]);
|
|
861
|
+
const [num, den] = fpsOut.trim().split("/").map(Number);
|
|
862
|
+
const originalFps = num / (den || 1);
|
|
863
|
+
|
|
864
|
+
// Calculate target FPS
|
|
865
|
+
const targetFps = options.fps ? parseInt(options.fps) : originalFps * factor;
|
|
866
|
+
|
|
867
|
+
// Use minterpolate for frame interpolation
|
|
868
|
+
const mi = options.quality === "fast" ? "mi_mode=mci" : "mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1";
|
|
869
|
+
|
|
870
|
+
spinner.text = `Interpolating frames (${originalFps.toFixed(1)} → ${targetFps}fps)...`;
|
|
871
|
+
|
|
872
|
+
// First interpolate frames, then slow down
|
|
873
|
+
await execSafe("ffmpeg", ["-i", absPath, "-filter:v", `minterpolate='${mi}:fps=${targetFps}',setpts=${factor}*PTS`, "-an", outputPath, "-y"], { timeout: 600000 });
|
|
874
|
+
|
|
875
|
+
spinner.succeed(chalk.green(`Created ${factor}x slow motion`));
|
|
876
|
+
|
|
877
|
+
if (isJsonMode()) {
|
|
878
|
+
outputResult({
|
|
879
|
+
success: true,
|
|
880
|
+
originalFps,
|
|
881
|
+
targetFps,
|
|
882
|
+
factor,
|
|
883
|
+
outputPath,
|
|
884
|
+
});
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
console.log();
|
|
889
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
890
|
+
console.log(`Original FPS: ${originalFps.toFixed(1)}`);
|
|
891
|
+
console.log(`Interpolated FPS: ${targetFps}`);
|
|
892
|
+
console.log(`Slow factor: ${factor}x`);
|
|
893
|
+
console.log(`Output: ${outputPath}`);
|
|
894
|
+
console.log();
|
|
895
|
+
} catch (err: unknown) {
|
|
896
|
+
spinner.fail(chalk.red("Frame interpolation failed"));
|
|
897
|
+
if (err instanceof Error && err.message.includes("timeout")) {
|
|
898
|
+
console.error(chalk.yellow("Processing timed out. Try with a shorter video or --quality fast"));
|
|
899
|
+
} else {
|
|
900
|
+
console.error(err);
|
|
901
|
+
}
|
|
902
|
+
process.exit(1);
|
|
903
|
+
}
|
|
904
|
+
} catch (error) {
|
|
905
|
+
console.error(chalk.red("Frame interpolation failed"));
|
|
906
|
+
console.error(error);
|
|
907
|
+
process.exit(1);
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// ── edit upscale-video (video upscaling) ────────────────────────────────
|
|
912
|
+
|
|
913
|
+
editCommand
|
|
914
|
+
.command("upscale-video")
|
|
915
|
+
.description("Upscale video resolution using AI or FFmpeg")
|
|
916
|
+
.argument("<video>", "Video file path")
|
|
917
|
+
.option("-o, --output <path>", "Output file path")
|
|
918
|
+
.option("-s, --scale <factor>", "Scale factor: 2 or 4", "2")
|
|
919
|
+
.option("-m, --model <model>", "Model: real-esrgan, topaz", "real-esrgan")
|
|
920
|
+
.option("--ffmpeg", "Use FFmpeg lanczos (free, no API)")
|
|
921
|
+
.option("-k, --api-key <key>", "Replicate API token (or set REPLICATE_API_TOKEN env)")
|
|
922
|
+
.option("--no-wait", "Start processing and return task ID without waiting")
|
|
923
|
+
.option("--dry-run", "Preview parameters without executing")
|
|
924
|
+
.action(async (videoPath: string, options) => {
|
|
925
|
+
try {
|
|
926
|
+
const absPath = resolve(process.cwd(), videoPath);
|
|
927
|
+
const scale = parseInt(options.scale);
|
|
928
|
+
|
|
929
|
+
if (scale !== 2 && scale !== 4) {
|
|
930
|
+
console.error(chalk.red("Scale must be 2 or 4"));
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (options.dryRun) {
|
|
935
|
+
outputResult({
|
|
936
|
+
dryRun: true,
|
|
937
|
+
command: "edit upscale-video",
|
|
938
|
+
params: {
|
|
939
|
+
videoPath: absPath,
|
|
940
|
+
scale,
|
|
941
|
+
model: options.model,
|
|
942
|
+
ffmpeg: options.ffmpeg || false,
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Use FFmpeg if requested (free fallback)
|
|
949
|
+
if (options.ffmpeg) {
|
|
950
|
+
const outputPath = options.output
|
|
951
|
+
? resolve(process.cwd(), options.output)
|
|
952
|
+
: absPath.replace(/(\.[^.]+)$/, `-upscaled-${scale}x$1`);
|
|
953
|
+
|
|
954
|
+
const spinner = ora(`Upscaling video with FFmpeg (${scale}x)...`).start();
|
|
955
|
+
|
|
956
|
+
try {
|
|
957
|
+
// Get original dimensions
|
|
958
|
+
const { stdout: probeOut } = await execSafe("ffprobe", [
|
|
959
|
+
"-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=p=0", absPath,
|
|
960
|
+
]);
|
|
961
|
+
const [width, height] = probeOut.trim().split(",").map(Number);
|
|
962
|
+
const newWidth = width * scale;
|
|
963
|
+
const newHeight = height * scale;
|
|
964
|
+
|
|
965
|
+
// Use lanczos scaling
|
|
966
|
+
await execSafe("ffmpeg", ["-i", absPath, "-vf", `scale=${newWidth}:${newHeight}:flags=lanczos`, "-c:a", "copy", outputPath, "-y"]);
|
|
967
|
+
|
|
968
|
+
spinner.succeed(chalk.green(`Upscaled to ${newWidth}x${newHeight}`));
|
|
969
|
+
|
|
970
|
+
if (isJsonMode()) {
|
|
971
|
+
outputResult({
|
|
972
|
+
success: true,
|
|
973
|
+
dimensions: `${newWidth}x${newHeight}`,
|
|
974
|
+
outputPath,
|
|
975
|
+
});
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
console.log(`Output: ${outputPath}`);
|
|
980
|
+
} catch (err) {
|
|
981
|
+
spinner.fail(chalk.red("FFmpeg upscaling failed"));
|
|
982
|
+
console.error(err);
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Use Replicate API
|
|
989
|
+
const apiKey = await requireApiKey("REPLICATE_API_TOKEN", "Replicate", options.apiKey);
|
|
990
|
+
|
|
991
|
+
const spinner = ora("Initializing Replicate...").start();
|
|
992
|
+
|
|
993
|
+
const { ReplicateProvider } = await import("@vibeframe/ai-providers");
|
|
994
|
+
const replicate = new ReplicateProvider();
|
|
995
|
+
await replicate.initialize({ apiKey });
|
|
996
|
+
|
|
997
|
+
// For Replicate, we need a URL. Upload to temporary hosting or require URL
|
|
998
|
+
spinner.text = "Note: Replicate requires video URL. Reading file...";
|
|
999
|
+
|
|
1000
|
+
// For now, we'll show an error suggesting URL or ffmpeg
|
|
1001
|
+
spinner.fail(chalk.yellow("Replicate requires a video URL"));
|
|
1002
|
+
console.log();
|
|
1003
|
+
console.log(chalk.dim("Options:"));
|
|
1004
|
+
console.log(chalk.dim(" 1. Use --ffmpeg for local processing"));
|
|
1005
|
+
console.log(chalk.dim(" 2. Upload video to a URL and run:"));
|
|
1006
|
+
console.log(chalk.dim(` pnpm vibe edit upscale-video https://example.com/video.mp4 -s ${scale}`));
|
|
1007
|
+
console.log();
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
console.error(chalk.red("Video upscaling failed"));
|
|
1011
|
+
console.error(error);
|
|
1012
|
+
process.exit(1);
|
|
1013
|
+
}
|
|
1014
|
+
});
|