@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,941 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module ai-audio
|
|
3
|
+
* @description Audio commands for the VibeFrame CLI.
|
|
4
|
+
*
|
|
5
|
+
* ## Commands: vibe ai transcribe, vibe ai tts, vibe ai voices, vibe ai sfx,
|
|
6
|
+
* vibe ai isolate, vibe ai voice-clone, vibe ai music,
|
|
7
|
+
* vibe ai music-status, vibe ai audio-restore, vibe ai dub, vibe ai duck
|
|
8
|
+
* ## Dependencies: Whisper, ElevenLabs, Replicate, FFmpeg
|
|
9
|
+
*
|
|
10
|
+
* Extracted from ai.ts as part of modularisation.
|
|
11
|
+
* ai.ts calls registerAudioCommands(aiCommand).
|
|
12
|
+
* @see MODELS.md for AI model configuration
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { type Command } from 'commander';
|
|
16
|
+
import { resolve, dirname, basename, extname } from 'node:path';
|
|
17
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
19
|
+
import chalk from 'chalk';
|
|
20
|
+
import ora from 'ora';
|
|
21
|
+
import {
|
|
22
|
+
WhisperProvider,
|
|
23
|
+
ElevenLabsProvider,
|
|
24
|
+
ReplicateProvider,
|
|
25
|
+
ClaudeProvider,
|
|
26
|
+
} from '@vibeframe/ai-providers';
|
|
27
|
+
import { getApiKey } from '../utils/api-key.js';
|
|
28
|
+
import { execSafe, execSafeSync, commandExists } from '../utils/exec-safe.js';
|
|
29
|
+
import { detectFormat, formatTranscript } from '../utils/subtitle.js';
|
|
30
|
+
import { formatTime } from './ai-helpers.js';
|
|
31
|
+
|
|
32
|
+
function _registerAudioCommands(aiCommand: Command): void {
|
|
33
|
+
|
|
34
|
+
aiCommand
|
|
35
|
+
.command("transcribe")
|
|
36
|
+
.description("Transcribe audio using Whisper")
|
|
37
|
+
.argument("<audio>", "Audio file path")
|
|
38
|
+
.option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)")
|
|
39
|
+
.option("-l, --language <lang>", "Language code (e.g., en, ko)")
|
|
40
|
+
.option("-o, --output <path>", "Output file path")
|
|
41
|
+
.option("-f, --format <format>", "Output format: json, srt, vtt (auto-detected from extension)")
|
|
42
|
+
.action(async (audioPath: string, options) => {
|
|
43
|
+
try {
|
|
44
|
+
const apiKey = await getApiKey("OPENAI_API_KEY", "OpenAI", options.apiKey);
|
|
45
|
+
if (!apiKey) {
|
|
46
|
+
console.error(chalk.red("OpenAI API key required. Set OPENAI_API_KEY in .env or run: vibe setup"));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const spinner = ora("Initializing Whisper...").start();
|
|
51
|
+
|
|
52
|
+
const whisper = new WhisperProvider();
|
|
53
|
+
await whisper.initialize({ apiKey });
|
|
54
|
+
|
|
55
|
+
spinner.text = "Reading audio file...";
|
|
56
|
+
const absPath = resolve(process.cwd(), audioPath);
|
|
57
|
+
const audioBuffer = await readFile(absPath);
|
|
58
|
+
const audioBlob = new Blob([audioBuffer]);
|
|
59
|
+
|
|
60
|
+
spinner.text = "Transcribing...";
|
|
61
|
+
const result = await whisper.transcribe(audioBlob, options.language);
|
|
62
|
+
|
|
63
|
+
if (result.status === "failed") {
|
|
64
|
+
spinner.fail(chalk.red(`Transcription failed: ${result.error}`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
spinner.succeed(chalk.green("Transcription complete"));
|
|
69
|
+
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(chalk.bold.cyan("Transcript"));
|
|
72
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
73
|
+
console.log(result.fullText);
|
|
74
|
+
console.log();
|
|
75
|
+
|
|
76
|
+
if (result.segments && result.segments.length > 0) {
|
|
77
|
+
console.log(chalk.bold.cyan("Segments"));
|
|
78
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
79
|
+
for (const seg of result.segments) {
|
|
80
|
+
const time = `[${formatTime(seg.startTime)} - ${formatTime(seg.endTime)}]`;
|
|
81
|
+
console.log(`${chalk.dim(time)} ${seg.text}`);
|
|
82
|
+
}
|
|
83
|
+
console.log();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (options.output) {
|
|
87
|
+
const outputPath = resolve(process.cwd(), options.output);
|
|
88
|
+
const format = detectFormat(options.output, options.format);
|
|
89
|
+
const content = formatTranscript(result, format);
|
|
90
|
+
await writeFile(outputPath, content, "utf-8");
|
|
91
|
+
console.log(chalk.green(`Saved ${format.toUpperCase()} to: ${outputPath}`));
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(chalk.red("Transcription failed"));
|
|
95
|
+
console.error(error);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
aiCommand
|
|
101
|
+
.command("tts")
|
|
102
|
+
.description("Generate speech from text using ElevenLabs")
|
|
103
|
+
.argument("<text>", "Text to convert to speech")
|
|
104
|
+
.option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)")
|
|
105
|
+
.option("-o, --output <path>", "Output audio file path", "output.mp3")
|
|
106
|
+
.option("-v, --voice <id>", "Voice ID (default: Rachel)", "21m00Tcm4TlvDq8ikWAM")
|
|
107
|
+
.option("--list-voices", "List available voices")
|
|
108
|
+
.action(async (text: string, options) => {
|
|
109
|
+
try {
|
|
110
|
+
const apiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
|
|
111
|
+
if (!apiKey) {
|
|
112
|
+
console.error(chalk.red("ElevenLabs API key required. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const elevenlabs = new ElevenLabsProvider();
|
|
117
|
+
await elevenlabs.initialize({ apiKey });
|
|
118
|
+
|
|
119
|
+
// List voices mode
|
|
120
|
+
if (options.listVoices) {
|
|
121
|
+
const spinner = ora("Fetching voices...").start();
|
|
122
|
+
const voices = await elevenlabs.getVoices();
|
|
123
|
+
spinner.succeed(chalk.green(`Found ${voices.length} voices`));
|
|
124
|
+
|
|
125
|
+
console.log();
|
|
126
|
+
console.log(chalk.bold.cyan("Available Voices"));
|
|
127
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
128
|
+
|
|
129
|
+
for (const voice of voices) {
|
|
130
|
+
console.log();
|
|
131
|
+
console.log(`${chalk.bold(voice.name)} ${chalk.dim(`(${voice.voice_id})`)}`);
|
|
132
|
+
console.log(` Category: ${voice.category}`);
|
|
133
|
+
if (voice.labels) {
|
|
134
|
+
const labels = Object.entries(voice.labels)
|
|
135
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
136
|
+
.join(", ");
|
|
137
|
+
console.log(` ${chalk.dim(labels)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
console.log();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const spinner = ora("Generating speech...").start();
|
|
145
|
+
|
|
146
|
+
const result = await elevenlabs.textToSpeech(text, {
|
|
147
|
+
voiceId: options.voice,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (!result.success || !result.audioBuffer) {
|
|
151
|
+
spinner.fail(chalk.red(result.error || "TTS generation failed"));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const outputPath = resolve(process.cwd(), options.output);
|
|
156
|
+
await writeFile(outputPath, result.audioBuffer);
|
|
157
|
+
|
|
158
|
+
spinner.succeed(chalk.green("Speech generated"));
|
|
159
|
+
console.log();
|
|
160
|
+
console.log(chalk.dim(`Characters: ${result.characterCount}`));
|
|
161
|
+
console.log(chalk.green(`Saved to: ${outputPath}`));
|
|
162
|
+
console.log();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(chalk.red("TTS generation failed"));
|
|
165
|
+
console.error(error);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
aiCommand
|
|
171
|
+
.command("voices")
|
|
172
|
+
.description("List available ElevenLabs voices")
|
|
173
|
+
.option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)")
|
|
174
|
+
.action(async (options) => {
|
|
175
|
+
try {
|
|
176
|
+
const apiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
|
|
177
|
+
if (!apiKey) {
|
|
178
|
+
console.error(chalk.red("ElevenLabs API key required. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const spinner = ora("Fetching voices...").start();
|
|
183
|
+
const elevenlabs = new ElevenLabsProvider();
|
|
184
|
+
await elevenlabs.initialize({ apiKey });
|
|
185
|
+
|
|
186
|
+
const voices = await elevenlabs.getVoices();
|
|
187
|
+
spinner.succeed(chalk.green(`Found ${voices.length} voices`));
|
|
188
|
+
|
|
189
|
+
console.log();
|
|
190
|
+
console.log(chalk.bold.cyan("Available Voices"));
|
|
191
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
192
|
+
|
|
193
|
+
for (const voice of voices) {
|
|
194
|
+
console.log();
|
|
195
|
+
console.log(`${chalk.bold(voice.name)} ${chalk.dim(`(${voice.voice_id})`)}`);
|
|
196
|
+
console.log(` Category: ${voice.category}`);
|
|
197
|
+
}
|
|
198
|
+
console.log();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(chalk.red("Failed to fetch voices"));
|
|
201
|
+
console.error(error);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
aiCommand
|
|
207
|
+
.command("sfx")
|
|
208
|
+
.description("Generate sound effect using ElevenLabs")
|
|
209
|
+
.argument("<prompt>", "Description of the sound effect")
|
|
210
|
+
.option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)")
|
|
211
|
+
.option("-o, --output <path>", "Output audio file path", "sound-effect.mp3")
|
|
212
|
+
.option("-d, --duration <seconds>", "Duration in seconds (0.5-22, default: auto)")
|
|
213
|
+
.option("--prompt-influence <value>", "Prompt influence (0-1, default: 0.3)")
|
|
214
|
+
.action(async (prompt: string, options) => {
|
|
215
|
+
try {
|
|
216
|
+
const apiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
|
|
217
|
+
if (!apiKey) {
|
|
218
|
+
console.error(chalk.red("ElevenLabs API key required. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const spinner = ora("Generating sound effect...").start();
|
|
223
|
+
|
|
224
|
+
const elevenlabs = new ElevenLabsProvider();
|
|
225
|
+
await elevenlabs.initialize({ apiKey });
|
|
226
|
+
|
|
227
|
+
const result = await elevenlabs.generateSoundEffect(prompt, {
|
|
228
|
+
duration: options.duration ? parseFloat(options.duration) : undefined,
|
|
229
|
+
promptInfluence: options.promptInfluence ? parseFloat(options.promptInfluence) : undefined,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (!result.success || !result.audioBuffer) {
|
|
233
|
+
spinner.fail(chalk.red(result.error || "Sound effect generation failed"));
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const outputPath = resolve(process.cwd(), options.output);
|
|
238
|
+
await writeFile(outputPath, result.audioBuffer);
|
|
239
|
+
|
|
240
|
+
spinner.succeed(chalk.green("Sound effect generated"));
|
|
241
|
+
console.log(chalk.green(`Saved to: ${outputPath}`));
|
|
242
|
+
console.log();
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error(chalk.red("Sound effect generation failed"));
|
|
245
|
+
console.error(error);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
aiCommand
|
|
251
|
+
.command("isolate")
|
|
252
|
+
.description("Isolate vocals from audio using ElevenLabs")
|
|
253
|
+
.argument("<audio>", "Input audio file path")
|
|
254
|
+
.option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)")
|
|
255
|
+
.option("-o, --output <path>", "Output audio file path", "vocals.mp3")
|
|
256
|
+
.action(async (audioPath: string, options) => {
|
|
257
|
+
try {
|
|
258
|
+
const apiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
|
|
259
|
+
if (!apiKey) {
|
|
260
|
+
console.error(chalk.red("ElevenLabs API key required. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const spinner = ora("Reading audio file...").start();
|
|
265
|
+
|
|
266
|
+
const absPath = resolve(process.cwd(), audioPath);
|
|
267
|
+
const audioBuffer = await readFile(absPath);
|
|
268
|
+
|
|
269
|
+
spinner.text = "Isolating vocals...";
|
|
270
|
+
|
|
271
|
+
const elevenlabs = new ElevenLabsProvider();
|
|
272
|
+
await elevenlabs.initialize({ apiKey });
|
|
273
|
+
|
|
274
|
+
const result = await elevenlabs.isolateVocals(audioBuffer);
|
|
275
|
+
|
|
276
|
+
if (!result.success || !result.audioBuffer) {
|
|
277
|
+
spinner.fail(chalk.red(result.error || "Audio isolation failed"));
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const outputPath = resolve(process.cwd(), options.output);
|
|
282
|
+
await writeFile(outputPath, result.audioBuffer);
|
|
283
|
+
|
|
284
|
+
spinner.succeed(chalk.green("Vocals isolated"));
|
|
285
|
+
console.log(chalk.green(`Saved to: ${outputPath}`));
|
|
286
|
+
console.log();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error(chalk.red("Audio isolation failed"));
|
|
289
|
+
console.error(error);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
aiCommand
|
|
296
|
+
.command("voice-clone")
|
|
297
|
+
.description("Clone a voice from audio samples using ElevenLabs")
|
|
298
|
+
.argument("[samples...]", "Audio sample files (1-25 files)")
|
|
299
|
+
.option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)")
|
|
300
|
+
.option("-n, --name <name>", "Voice name (required)")
|
|
301
|
+
.option("-d, --description <desc>", "Voice description")
|
|
302
|
+
.option("--labels <json>", "Labels as JSON (e.g., '{\"accent\": \"american\"}')")
|
|
303
|
+
.option("--remove-noise", "Remove background noise from samples")
|
|
304
|
+
.option("--list", "List all available voices")
|
|
305
|
+
.action(async (samples: string[], options) => {
|
|
306
|
+
try {
|
|
307
|
+
const apiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
|
|
308
|
+
if (!apiKey) {
|
|
309
|
+
console.error(chalk.red("ElevenLabs API key required. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const elevenlabs = new ElevenLabsProvider();
|
|
314
|
+
await elevenlabs.initialize({ apiKey });
|
|
315
|
+
|
|
316
|
+
// List voices mode
|
|
317
|
+
if (options.list) {
|
|
318
|
+
const spinner = ora("Fetching voices...").start();
|
|
319
|
+
const voices = await elevenlabs.getVoices();
|
|
320
|
+
spinner.succeed(chalk.green(`Found ${voices.length} voices`));
|
|
321
|
+
|
|
322
|
+
console.log();
|
|
323
|
+
console.log(chalk.bold.cyan("Available Voices"));
|
|
324
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
325
|
+
|
|
326
|
+
for (const voice of voices) {
|
|
327
|
+
const category = chalk.dim(`(${voice.category})`);
|
|
328
|
+
console.log(`${chalk.bold(voice.name)} ${category}`);
|
|
329
|
+
console.log(` ${chalk.dim("ID:")} ${voice.voice_id}`);
|
|
330
|
+
if (voice.labels && Object.keys(voice.labels).length > 0) {
|
|
331
|
+
console.log(` ${chalk.dim("Labels:")} ${JSON.stringify(voice.labels)}`);
|
|
332
|
+
}
|
|
333
|
+
console.log();
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Clone voice mode
|
|
339
|
+
if (!options.name) {
|
|
340
|
+
console.error(chalk.red("Voice name is required. Use --name <name>"));
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!samples || samples.length === 0) {
|
|
345
|
+
console.error(chalk.red("At least one audio sample is required"));
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const spinner = ora("Reading audio samples...").start();
|
|
350
|
+
|
|
351
|
+
const audioBuffers: Buffer[] = [];
|
|
352
|
+
for (const samplePath of samples) {
|
|
353
|
+
const absPath = resolve(process.cwd(), samplePath);
|
|
354
|
+
if (!existsSync(absPath)) {
|
|
355
|
+
spinner.fail(chalk.red(`File not found: ${samplePath}`));
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
const buffer = await readFile(absPath);
|
|
359
|
+
audioBuffers.push(buffer);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
spinner.text = `Cloning voice from ${audioBuffers.length} sample(s)...`;
|
|
363
|
+
|
|
364
|
+
const labels = options.labels ? JSON.parse(options.labels) : undefined;
|
|
365
|
+
|
|
366
|
+
const result = await elevenlabs.cloneVoice(audioBuffers, {
|
|
367
|
+
name: options.name,
|
|
368
|
+
description: options.description,
|
|
369
|
+
labels,
|
|
370
|
+
removeBackgroundNoise: options.removeNoise,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (!result.success) {
|
|
374
|
+
spinner.fail(chalk.red(result.error || "Voice cloning failed"));
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
spinner.succeed(chalk.green("Voice cloned successfully"));
|
|
379
|
+
console.log();
|
|
380
|
+
console.log(chalk.bold.cyan("Voice Details"));
|
|
381
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
382
|
+
console.log(`Name: ${chalk.bold(options.name)}`);
|
|
383
|
+
console.log(`Voice ID: ${chalk.bold(result.voiceId)}`);
|
|
384
|
+
console.log();
|
|
385
|
+
console.log(chalk.dim("Use this voice ID with:"));
|
|
386
|
+
console.log(chalk.dim(` pnpm vibe ai tts "Hello world" -v ${result.voiceId}`));
|
|
387
|
+
console.log();
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error(chalk.red("Voice cloning failed"));
|
|
390
|
+
console.error(error);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
aiCommand
|
|
396
|
+
.command("music")
|
|
397
|
+
.description("Generate background music from a text prompt using MusicGen")
|
|
398
|
+
.argument("<prompt>", "Description of the music to generate")
|
|
399
|
+
.option("-k, --api-key <key>", "Replicate API token (or set REPLICATE_API_TOKEN env)")
|
|
400
|
+
.option("-d, --duration <seconds>", "Duration in seconds (1-30)", "8")
|
|
401
|
+
.option("-m, --melody <file>", "Reference melody audio file for conditioning")
|
|
402
|
+
.option("--model <model>", "Model variant: large, stereo-large, melody-large, stereo-melody-large", "stereo-large")
|
|
403
|
+
.option("-o, --output <path>", "Output audio file path", "music.mp3")
|
|
404
|
+
.option("--no-wait", "Don't wait for generation to complete (async mode)")
|
|
405
|
+
.action(async (prompt: string, options) => {
|
|
406
|
+
try {
|
|
407
|
+
const apiKey = await getApiKey("REPLICATE_API_TOKEN", "Replicate", options.apiKey);
|
|
408
|
+
if (!apiKey) {
|
|
409
|
+
console.error(chalk.red("Replicate API token required. Set REPLICATE_API_TOKEN in .env or run: vibe setup"));
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const replicate = new ReplicateProvider();
|
|
414
|
+
await replicate.initialize({ apiKey });
|
|
415
|
+
|
|
416
|
+
const spinner = ora("Starting music generation...").start();
|
|
417
|
+
|
|
418
|
+
const duration = Math.max(1, Math.min(30, parseFloat(options.duration)));
|
|
419
|
+
|
|
420
|
+
// If melody file provided, upload it first
|
|
421
|
+
let melodyUrl: string | undefined;
|
|
422
|
+
if (options.melody) {
|
|
423
|
+
spinner.text = "Uploading melody reference...";
|
|
424
|
+
const absPath = resolve(process.cwd(), options.melody);
|
|
425
|
+
if (!existsSync(absPath)) {
|
|
426
|
+
spinner.fail(chalk.red(`Melody file not found: ${options.melody}`));
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
// For Replicate, we need a publicly accessible URL
|
|
430
|
+
// In practice, users would need to host the file or use a data URL
|
|
431
|
+
console.log(chalk.yellow("Note: Melody conditioning requires a publicly accessible URL"));
|
|
432
|
+
console.log(chalk.yellow("Please upload your melody file and provide the URL"));
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const result = await replicate.generateMusic(prompt, {
|
|
437
|
+
duration,
|
|
438
|
+
model: options.model as "large" | "stereo-large" | "melody-large" | "stereo-melody-large",
|
|
439
|
+
melodyUrl,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (!result.success || !result.taskId) {
|
|
443
|
+
spinner.fail(chalk.red(result.error || "Music generation failed"));
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!options.wait) {
|
|
448
|
+
spinner.succeed(chalk.green("Music generation started"));
|
|
449
|
+
console.log();
|
|
450
|
+
console.log(`Task ID: ${chalk.bold(result.taskId)}`);
|
|
451
|
+
console.log(chalk.dim("Check status with: pnpm vibe ai music-status " + result.taskId));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
spinner.text = "Generating music (this may take a few minutes)...";
|
|
456
|
+
|
|
457
|
+
const finalResult = await replicate.waitForMusic(result.taskId);
|
|
458
|
+
|
|
459
|
+
if (!finalResult.success || !finalResult.audioUrl) {
|
|
460
|
+
spinner.fail(chalk.red(finalResult.error || "Music generation failed"));
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
spinner.text = "Downloading generated audio...";
|
|
465
|
+
|
|
466
|
+
const response = await fetch(finalResult.audioUrl);
|
|
467
|
+
if (!response.ok) {
|
|
468
|
+
spinner.fail(chalk.red("Failed to download generated audio"));
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const audioBuffer = Buffer.from(await response.arrayBuffer());
|
|
473
|
+
const outputPath = resolve(process.cwd(), options.output);
|
|
474
|
+
await writeFile(outputPath, audioBuffer);
|
|
475
|
+
|
|
476
|
+
spinner.succeed(chalk.green("Music generated successfully"));
|
|
477
|
+
console.log();
|
|
478
|
+
console.log(`Saved to: ${chalk.bold(outputPath)}`);
|
|
479
|
+
console.log(`Duration: ${duration}s`);
|
|
480
|
+
console.log(`Model: ${options.model}`);
|
|
481
|
+
console.log();
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error(chalk.red("Music generation failed"));
|
|
484
|
+
console.error(error);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
aiCommand
|
|
490
|
+
.command("music-status")
|
|
491
|
+
.description("Check music generation status")
|
|
492
|
+
.argument("<task-id>", "Task ID from music generation")
|
|
493
|
+
.option("-k, --api-key <key>", "Replicate API token (or set REPLICATE_API_TOKEN env)")
|
|
494
|
+
.action(async (taskId: string, options) => {
|
|
495
|
+
try {
|
|
496
|
+
const apiKey = await getApiKey("REPLICATE_API_TOKEN", "Replicate", options.apiKey);
|
|
497
|
+
if (!apiKey) {
|
|
498
|
+
console.error(chalk.red("Replicate API token required. Set REPLICATE_API_TOKEN in .env or run: vibe setup"));
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const replicate = new ReplicateProvider();
|
|
503
|
+
await replicate.initialize({ apiKey });
|
|
504
|
+
|
|
505
|
+
const result = await replicate.getMusicStatus(taskId);
|
|
506
|
+
|
|
507
|
+
console.log();
|
|
508
|
+
console.log(chalk.bold.cyan("Music Generation Status"));
|
|
509
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
510
|
+
console.log(`Task ID: ${taskId}`);
|
|
511
|
+
|
|
512
|
+
if (result.audioUrl) {
|
|
513
|
+
console.log(`Status: ${chalk.green("completed")}`);
|
|
514
|
+
console.log(`Audio URL: ${result.audioUrl}`);
|
|
515
|
+
} else if (result.error) {
|
|
516
|
+
console.log(`Status: ${chalk.red("failed")}`);
|
|
517
|
+
console.log(`Error: ${result.error}`);
|
|
518
|
+
} else {
|
|
519
|
+
console.log(`Status: ${chalk.yellow("processing")}`);
|
|
520
|
+
}
|
|
521
|
+
console.log();
|
|
522
|
+
} catch (error) {
|
|
523
|
+
console.error(chalk.red("Failed to get music status"));
|
|
524
|
+
console.error(error);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
aiCommand
|
|
530
|
+
.command("audio-restore")
|
|
531
|
+
.description("Restore audio quality (denoise, enhance)")
|
|
532
|
+
.argument("<audio>", "Input audio file path")
|
|
533
|
+
.option("-k, --api-key <key>", "Replicate API token (or set REPLICATE_API_TOKEN env)")
|
|
534
|
+
.option("-o, --output <path>", "Output audio file path")
|
|
535
|
+
.option("--ffmpeg", "Use FFmpeg for restoration (free, no API needed)")
|
|
536
|
+
.option("--denoise", "Enable noise reduction (default: true)", true)
|
|
537
|
+
.option("--no-denoise", "Disable noise reduction")
|
|
538
|
+
.option("--enhance", "Enable audio enhancement")
|
|
539
|
+
.option("--noise-floor <dB>", "FFmpeg noise floor threshold", "-30")
|
|
540
|
+
.action(async (audioPath: string, options) => {
|
|
541
|
+
try {
|
|
542
|
+
const absPath = resolve(process.cwd(), audioPath);
|
|
543
|
+
if (!existsSync(absPath)) {
|
|
544
|
+
console.error(chalk.red(`File not found: ${audioPath}`));
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Default output path
|
|
549
|
+
const ext = extname(audioPath);
|
|
550
|
+
const baseName = basename(audioPath, ext);
|
|
551
|
+
const defaultOutput = `${baseName}-restored${ext || ".mp3"}`;
|
|
552
|
+
const outputPath = resolve(process.cwd(), options.output || defaultOutput);
|
|
553
|
+
|
|
554
|
+
// FFmpeg mode (free)
|
|
555
|
+
if (options.ffmpeg) {
|
|
556
|
+
const spinner = ora("Restoring audio with FFmpeg...").start();
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
const noiseFloor = options.noiseFloor || "-30";
|
|
560
|
+
|
|
561
|
+
// Build filter chain
|
|
562
|
+
const filters: string[] = [];
|
|
563
|
+
|
|
564
|
+
if (options.denoise !== false) {
|
|
565
|
+
filters.push(`afftdn=nf=${noiseFloor}`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (options.enhance) {
|
|
569
|
+
filters.push("highpass=f=80");
|
|
570
|
+
filters.push("lowpass=f=12000");
|
|
571
|
+
filters.push("loudnorm=I=-16:TP=-1.5:LRA=11");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const ffmpegArgs = ["-i", absPath];
|
|
575
|
+
if (filters.length > 0) {
|
|
576
|
+
ffmpegArgs.push("-af", filters.join(","));
|
|
577
|
+
}
|
|
578
|
+
ffmpegArgs.push("-y", outputPath);
|
|
579
|
+
|
|
580
|
+
execSafeSync("ffmpeg", ffmpegArgs);
|
|
581
|
+
|
|
582
|
+
spinner.succeed(chalk.green("Audio restored with FFmpeg"));
|
|
583
|
+
console.log(`Saved to: ${chalk.bold(outputPath)}`);
|
|
584
|
+
console.log();
|
|
585
|
+
} catch (error) {
|
|
586
|
+
spinner.fail(chalk.red("FFmpeg restoration failed"));
|
|
587
|
+
if (error instanceof Error && "message" in error) {
|
|
588
|
+
console.error(chalk.dim(error.message));
|
|
589
|
+
}
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Replicate AI mode
|
|
596
|
+
const apiKey = await getApiKey("REPLICATE_API_TOKEN", "Replicate", options.apiKey);
|
|
597
|
+
if (!apiKey) {
|
|
598
|
+
console.error(chalk.red("Replicate API token required. Set REPLICATE_API_TOKEN in .env or run: vibe setup"));
|
|
599
|
+
console.error(chalk.dim("Or use --ffmpeg for free FFmpeg-based restoration"));
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const replicate = new ReplicateProvider();
|
|
604
|
+
await replicate.initialize({ apiKey });
|
|
605
|
+
|
|
606
|
+
// For Replicate, we need a publicly accessible URL
|
|
607
|
+
// This is a limitation - users need to upload their file first
|
|
608
|
+
console.log(chalk.yellow("Note: Replicate requires a publicly accessible audio URL"));
|
|
609
|
+
console.log(chalk.yellow("For local files, use --ffmpeg for free local processing"));
|
|
610
|
+
console.log();
|
|
611
|
+
console.log(chalk.dim("Example with FFmpeg:"));
|
|
612
|
+
console.log(chalk.dim(` pnpm vibe ai audio-restore ${audioPath} --ffmpeg`));
|
|
613
|
+
process.exit(1);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
console.error(chalk.red("Audio restoration failed"));
|
|
616
|
+
console.error(error);
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
aiCommand
|
|
622
|
+
.command("dub")
|
|
623
|
+
.description("Dub audio/video to another language (transcribe, translate, TTS)")
|
|
624
|
+
.argument("<media>", "Input media file (video or audio)")
|
|
625
|
+
.option("-l, --language <lang>", "Target language code (e.g., es, ko, ja) (required)")
|
|
626
|
+
.option("--source <lang>", "Source language code (default: auto-detect)")
|
|
627
|
+
.option("-v, --voice <id>", "ElevenLabs voice ID for output")
|
|
628
|
+
.option("--analyze-only", "Only analyze and show timing, don't generate audio")
|
|
629
|
+
.option("-o, --output <path>", "Output file path")
|
|
630
|
+
.action(async (mediaPath: string, options) => {
|
|
631
|
+
try {
|
|
632
|
+
if (!options.language) {
|
|
633
|
+
console.error(chalk.red("Target language is required. Use -l or --language"));
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const absPath = resolve(process.cwd(), mediaPath);
|
|
638
|
+
if (!existsSync(absPath)) {
|
|
639
|
+
console.error(chalk.red(`File not found: ${mediaPath}`));
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Check required API keys
|
|
644
|
+
const openaiKey = await getApiKey("OPENAI_API_KEY", "OpenAI", undefined);
|
|
645
|
+
const anthropicKey = await getApiKey("ANTHROPIC_API_KEY", "Anthropic", undefined);
|
|
646
|
+
const elevenlabsKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs", undefined);
|
|
647
|
+
|
|
648
|
+
if (!openaiKey) {
|
|
649
|
+
console.error(chalk.red("OpenAI API key required for transcription. Set OPENAI_API_KEY in .env or run: vibe setup"));
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (!anthropicKey) {
|
|
654
|
+
console.error(chalk.red("Anthropic API key required for translation. Set ANTHROPIC_API_KEY in .env or run: vibe setup"));
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (!options.analyzeOnly && !elevenlabsKey) {
|
|
659
|
+
console.error(chalk.red("ElevenLabs API key required for TTS. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
|
|
660
|
+
console.error(chalk.dim("Or use --analyze-only to preview timing without generating audio"));
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const spinner = ora("Extracting audio...").start();
|
|
665
|
+
|
|
666
|
+
// Check if input is video
|
|
667
|
+
const ext = extname(absPath).toLowerCase();
|
|
668
|
+
const isVideo = [".mp4", ".mov", ".avi", ".mkv", ".webm"].includes(ext);
|
|
669
|
+
|
|
670
|
+
// Step 1: Extract audio if video
|
|
671
|
+
let audioPath = absPath;
|
|
672
|
+
if (isVideo) {
|
|
673
|
+
const tempAudioPath = resolve(dirname(absPath), `temp-audio-${Date.now()}.mp3`);
|
|
674
|
+
try {
|
|
675
|
+
execSafeSync("ffmpeg", ["-i", absPath, "-vn", "-acodec", "mp3", "-y", tempAudioPath]);
|
|
676
|
+
audioPath = tempAudioPath;
|
|
677
|
+
} catch (error) {
|
|
678
|
+
spinner.fail(chalk.red("Failed to extract audio from video"));
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Step 2: Transcribe with Whisper
|
|
684
|
+
spinner.text = "Transcribing audio...";
|
|
685
|
+
const whisper = new WhisperProvider();
|
|
686
|
+
await whisper.initialize({ apiKey: openaiKey });
|
|
687
|
+
|
|
688
|
+
const audioBuffer = await readFile(audioPath);
|
|
689
|
+
const audioBlob = new Blob([audioBuffer]);
|
|
690
|
+
|
|
691
|
+
const transcriptResult = await whisper.transcribe(audioBlob, options.source);
|
|
692
|
+
|
|
693
|
+
if (transcriptResult.status === "failed" || !transcriptResult.segments) {
|
|
694
|
+
spinner.fail(chalk.red(`Transcription failed: ${transcriptResult.error}`));
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Step 3: Translate with Claude
|
|
699
|
+
spinner.text = "Translating...";
|
|
700
|
+
const claude = new ClaudeProvider();
|
|
701
|
+
await claude.initialize({ apiKey: anthropicKey });
|
|
702
|
+
|
|
703
|
+
// Build translation prompt
|
|
704
|
+
const segments = transcriptResult.segments;
|
|
705
|
+
const segmentTexts = segments.map((s, i) => `[${i}] ${s.text}`).join("\n");
|
|
706
|
+
|
|
707
|
+
// Language names for better translation context
|
|
708
|
+
const languageNames: Record<string, string> = {
|
|
709
|
+
en: "English", es: "Spanish", fr: "French", de: "German",
|
|
710
|
+
it: "Italian", pt: "Portuguese", ja: "Japanese", ko: "Korean",
|
|
711
|
+
zh: "Chinese", ar: "Arabic", ru: "Russian", hi: "Hindi",
|
|
712
|
+
};
|
|
713
|
+
const targetLangName = languageNames[options.language] || options.language;
|
|
714
|
+
|
|
715
|
+
// Use Claude's analyzeContent method to translate the segments
|
|
716
|
+
// The segments maintain their timing, we just need translated text
|
|
717
|
+
let translatedSegments: Array<{ index: number; text: string; startTime: number; endTime: number }> = [];
|
|
718
|
+
|
|
719
|
+
try {
|
|
720
|
+
// For translation, we use analyzeContent with a custom prompt
|
|
721
|
+
// This returns storyboard segments which we can adapt for translation
|
|
722
|
+
const storyboard = await claude.analyzeContent(
|
|
723
|
+
`TRANSLATE to ${targetLangName}. Return the translated text only, preserving segment numbers:\n\n${segmentTexts}`,
|
|
724
|
+
segments[segments.length - 1]?.endTime || 60
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
// Map storyboard results to translated segments
|
|
728
|
+
// If storyboard returned results, use descriptions as translations
|
|
729
|
+
if (storyboard && storyboard.length > 0) {
|
|
730
|
+
translatedSegments = segments.map((s, i) => ({
|
|
731
|
+
index: i,
|
|
732
|
+
text: storyboard[i]?.description || s.text,
|
|
733
|
+
startTime: s.startTime,
|
|
734
|
+
endTime: s.endTime,
|
|
735
|
+
}));
|
|
736
|
+
} else {
|
|
737
|
+
// Fallback: use original text
|
|
738
|
+
translatedSegments = segments.map((s, i) => ({
|
|
739
|
+
index: i,
|
|
740
|
+
text: s.text,
|
|
741
|
+
startTime: s.startTime,
|
|
742
|
+
endTime: s.endTime,
|
|
743
|
+
}));
|
|
744
|
+
}
|
|
745
|
+
} catch {
|
|
746
|
+
// Fallback: just show original text
|
|
747
|
+
translatedSegments = segments.map((s, i) => ({
|
|
748
|
+
index: i,
|
|
749
|
+
text: s.text,
|
|
750
|
+
startTime: s.startTime,
|
|
751
|
+
endTime: s.endTime,
|
|
752
|
+
}));
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
spinner.succeed(chalk.green("Transcription and translation complete"));
|
|
756
|
+
|
|
757
|
+
// Display timing analysis
|
|
758
|
+
console.log();
|
|
759
|
+
console.log(chalk.bold.cyan("Dubbing Analysis"));
|
|
760
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
761
|
+
console.log(`Source language: ${transcriptResult.detectedLanguage || options.source || "auto"}`);
|
|
762
|
+
console.log(`Target language: ${targetLangName}`);
|
|
763
|
+
console.log(`Segments: ${segments.length}`);
|
|
764
|
+
console.log();
|
|
765
|
+
|
|
766
|
+
console.log(chalk.bold("Segment Timing:"));
|
|
767
|
+
for (let i = 0; i < Math.min(5, segments.length); i++) {
|
|
768
|
+
const seg = segments[i];
|
|
769
|
+
const time = `[${formatTime(seg.startTime)} - ${formatTime(seg.endTime)}]`;
|
|
770
|
+
console.log(`${chalk.dim(time)} ${seg.text}`);
|
|
771
|
+
console.log(`${chalk.dim(" →")} ${chalk.green(translatedSegments[i]?.text || seg.text)}`);
|
|
772
|
+
console.log();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (segments.length > 5) {
|
|
776
|
+
console.log(chalk.dim(`... and ${segments.length - 5} more segments`));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (options.analyzeOnly) {
|
|
780
|
+
console.log();
|
|
781
|
+
console.log(chalk.dim("Use without --analyze-only to generate dubbed audio"));
|
|
782
|
+
|
|
783
|
+
// Save timing to JSON if output specified
|
|
784
|
+
if (options.output) {
|
|
785
|
+
const timingPath = resolve(process.cwd(), options.output);
|
|
786
|
+
const timingData = {
|
|
787
|
+
sourcePath: absPath,
|
|
788
|
+
sourceLanguage: transcriptResult.detectedLanguage || options.source || "auto",
|
|
789
|
+
targetLanguage: options.language,
|
|
790
|
+
segments: segments.map((s, i) => ({
|
|
791
|
+
index: i,
|
|
792
|
+
startTime: s.startTime,
|
|
793
|
+
endTime: s.endTime,
|
|
794
|
+
original: s.text,
|
|
795
|
+
translated: translatedSegments[i]?.text || s.text,
|
|
796
|
+
})),
|
|
797
|
+
};
|
|
798
|
+
await writeFile(timingPath, JSON.stringify(timingData, null, 2));
|
|
799
|
+
console.log(`Timing saved to: ${chalk.bold(timingPath)}`);
|
|
800
|
+
}
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Step 4: Generate TTS for each segment
|
|
805
|
+
spinner.start("Generating dubbed audio...");
|
|
806
|
+
const elevenlabs = new ElevenLabsProvider();
|
|
807
|
+
await elevenlabs.initialize({ apiKey: elevenlabsKey! });
|
|
808
|
+
|
|
809
|
+
const dubbedAudioBuffers: Array<{ buffer: Buffer; startTime: number }> = [];
|
|
810
|
+
|
|
811
|
+
for (let i = 0; i < translatedSegments.length; i++) {
|
|
812
|
+
spinner.text = `Generating audio segment ${i + 1}/${translatedSegments.length}...`;
|
|
813
|
+
const seg = translatedSegments[i];
|
|
814
|
+
|
|
815
|
+
const ttsResult = await elevenlabs.textToSpeech(seg.text, {
|
|
816
|
+
voiceId: options.voice,
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
if (ttsResult.success && ttsResult.audioBuffer) {
|
|
820
|
+
dubbedAudioBuffers.push({
|
|
821
|
+
buffer: ttsResult.audioBuffer,
|
|
822
|
+
startTime: seg.startTime,
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Step 5: Combine and save
|
|
828
|
+
spinner.text = "Combining audio...";
|
|
829
|
+
|
|
830
|
+
// For simplicity, just concatenate the audio buffers
|
|
831
|
+
// In production, you'd use FFmpeg to properly place them at timestamps
|
|
832
|
+
const combinedBuffer = Buffer.concat(dubbedAudioBuffers.map((a) => a.buffer));
|
|
833
|
+
|
|
834
|
+
const outputExt = isVideo ? ".mp3" : extname(absPath);
|
|
835
|
+
const defaultOutputPath = resolve(
|
|
836
|
+
dirname(absPath),
|
|
837
|
+
`${basename(absPath, extname(absPath))}-${options.language}${outputExt}`
|
|
838
|
+
);
|
|
839
|
+
const finalOutputPath = resolve(process.cwd(), options.output || defaultOutputPath);
|
|
840
|
+
|
|
841
|
+
await writeFile(finalOutputPath, combinedBuffer);
|
|
842
|
+
|
|
843
|
+
spinner.succeed(chalk.green("Dubbing complete"));
|
|
844
|
+
console.log();
|
|
845
|
+
console.log(`Saved to: ${chalk.bold(finalOutputPath)}`);
|
|
846
|
+
console.log();
|
|
847
|
+
|
|
848
|
+
// Clean up temp audio if we extracted from video
|
|
849
|
+
if (isVideo && audioPath !== absPath) {
|
|
850
|
+
try {
|
|
851
|
+
const { unlink } = await import("node:fs/promises");
|
|
852
|
+
await unlink(audioPath);
|
|
853
|
+
} catch {
|
|
854
|
+
// Ignore cleanup errors
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
} catch (error) {
|
|
858
|
+
console.error(chalk.red("Dubbing failed"));
|
|
859
|
+
console.error(error);
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// ============================================
|
|
865
|
+
// Smart Editing Commands
|
|
866
|
+
// ============================================
|
|
867
|
+
|
|
868
|
+
// Audio Ducking (FFmpeg only)
|
|
869
|
+
aiCommand
|
|
870
|
+
.command("duck")
|
|
871
|
+
.description("Auto-duck background music when voice is present (FFmpeg)")
|
|
872
|
+
.argument("<music>", "Background music file path")
|
|
873
|
+
.option("-v, --voice <path>", "Voice/narration track (required)")
|
|
874
|
+
.option("-o, --output <path>", "Output audio file path")
|
|
875
|
+
.option("-t, --threshold <dB>", "Sidechain threshold in dB", "-30")
|
|
876
|
+
.option("-r, --ratio <ratio>", "Compression ratio", "3")
|
|
877
|
+
.option("-a, --attack <ms>", "Attack time in ms", "20")
|
|
878
|
+
.option("-l, --release <ms>", "Release time in ms", "200")
|
|
879
|
+
.action(async (musicPath: string, options) => {
|
|
880
|
+
try {
|
|
881
|
+
if (!options.voice) {
|
|
882
|
+
console.error(chalk.red("Voice track required. Use --voice <path>"));
|
|
883
|
+
process.exit(1);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Check FFmpeg availability
|
|
887
|
+
if (!commandExists("ffmpeg")) {
|
|
888
|
+
console.error(chalk.red("FFmpeg not found. Please install FFmpeg."));
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const spinner = ora("Processing audio ducking...").start();
|
|
893
|
+
|
|
894
|
+
const absMusic = resolve(process.cwd(), musicPath);
|
|
895
|
+
const absVoice = resolve(process.cwd(), options.voice);
|
|
896
|
+
const outputPath = options.output
|
|
897
|
+
? resolve(process.cwd(), options.output)
|
|
898
|
+
: absMusic.replace(/(\.[^.]+)$/, "-ducked$1");
|
|
899
|
+
|
|
900
|
+
// Convert threshold from dB to linear (0-1 scale)
|
|
901
|
+
const thresholdDb = parseFloat(options.threshold);
|
|
902
|
+
const thresholdLinear = Math.pow(10, thresholdDb / 20);
|
|
903
|
+
|
|
904
|
+
const ratio = parseFloat(options.ratio);
|
|
905
|
+
const attack = parseFloat(options.attack);
|
|
906
|
+
const release = parseFloat(options.release);
|
|
907
|
+
|
|
908
|
+
// FFmpeg sidechain compress filter
|
|
909
|
+
const filterComplex = `[0:a][1:a]sidechaincompress=threshold=${thresholdLinear}:ratio=${ratio}:attack=${attack}:release=${release}[out]`;
|
|
910
|
+
|
|
911
|
+
await execSafe("ffmpeg", ["-i", absMusic, "-i", absVoice, "-filter_complex", filterComplex, "-map", "[out]", outputPath, "-y"]);
|
|
912
|
+
|
|
913
|
+
spinner.succeed(chalk.green("Audio ducking complete"));
|
|
914
|
+
console.log();
|
|
915
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
916
|
+
console.log(`Music: ${musicPath}`);
|
|
917
|
+
console.log(`Voice: ${options.voice}`);
|
|
918
|
+
console.log(`Threshold: ${thresholdDb}dB`);
|
|
919
|
+
console.log(`Ratio: ${ratio}:1`);
|
|
920
|
+
console.log(`Attack/Release: ${attack}ms / ${release}ms`);
|
|
921
|
+
console.log();
|
|
922
|
+
console.log(chalk.green(`Output: ${outputPath}`));
|
|
923
|
+
console.log();
|
|
924
|
+
} catch (error) {
|
|
925
|
+
console.error(chalk.red("Audio ducking failed"));
|
|
926
|
+
console.error(error);
|
|
927
|
+
process.exit(1);
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// AI Color Grading
|
|
932
|
+
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Register all audio sub-commands on the given parent command.
|
|
937
|
+
* Called from ai.ts: registerAudioCommands(aiCommand)
|
|
938
|
+
*/
|
|
939
|
+
export function registerAudioCommands(aiCommand: Command): void {
|
|
940
|
+
_registerAudioCommands(aiCommand);
|
|
941
|
+
}
|