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