@vibeframe/cli 0.54.0 → 0.55.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. package/dist/index.js +63452 -233
  2. package/package.json +11 -6
  3. package/dist/agent/adapters/claude.d.ts +0 -15
  4. package/dist/agent/adapters/claude.d.ts.map +0 -1
  5. package/dist/agent/adapters/claude.js +0 -119
  6. package/dist/agent/adapters/claude.js.map +0 -1
  7. package/dist/agent/adapters/gemini.d.ts +0 -15
  8. package/dist/agent/adapters/gemini.d.ts.map +0 -1
  9. package/dist/agent/adapters/gemini.js +0 -132
  10. package/dist/agent/adapters/gemini.js.map +0 -1
  11. package/dist/agent/adapters/index.d.ts +0 -28
  12. package/dist/agent/adapters/index.d.ts.map +0 -1
  13. package/dist/agent/adapters/index.js +0 -43
  14. package/dist/agent/adapters/index.js.map +0 -1
  15. package/dist/agent/adapters/ollama.d.ts +0 -20
  16. package/dist/agent/adapters/ollama.d.ts.map +0 -1
  17. package/dist/agent/adapters/ollama.js +0 -186
  18. package/dist/agent/adapters/ollama.js.map +0 -1
  19. package/dist/agent/adapters/openai.d.ts +0 -15
  20. package/dist/agent/adapters/openai.d.ts.map +0 -1
  21. package/dist/agent/adapters/openai.js +0 -92
  22. package/dist/agent/adapters/openai.js.map +0 -1
  23. package/dist/agent/adapters/openrouter.d.ts +0 -16
  24. package/dist/agent/adapters/openrouter.d.ts.map +0 -1
  25. package/dist/agent/adapters/openrouter.js +0 -100
  26. package/dist/agent/adapters/openrouter.js.map +0 -1
  27. package/dist/agent/adapters/openrouter.test.d.ts +0 -5
  28. package/dist/agent/adapters/openrouter.test.d.ts.map +0 -1
  29. package/dist/agent/adapters/openrouter.test.js +0 -73
  30. package/dist/agent/adapters/openrouter.test.js.map +0 -1
  31. package/dist/agent/adapters/xai.d.ts +0 -15
  32. package/dist/agent/adapters/xai.d.ts.map +0 -1
  33. package/dist/agent/adapters/xai.js +0 -95
  34. package/dist/agent/adapters/xai.js.map +0 -1
  35. package/dist/agent/index.d.ts +0 -69
  36. package/dist/agent/index.d.ts.map +0 -1
  37. package/dist/agent/index.js +0 -180
  38. package/dist/agent/index.js.map +0 -1
  39. package/dist/agent/memory/index.d.ts +0 -70
  40. package/dist/agent/memory/index.d.ts.map +0 -1
  41. package/dist/agent/memory/index.js +0 -132
  42. package/dist/agent/memory/index.js.map +0 -1
  43. package/dist/agent/prompts/system.d.ts +0 -6
  44. package/dist/agent/prompts/system.d.ts.map +0 -1
  45. package/dist/agent/prompts/system.js +0 -103
  46. package/dist/agent/prompts/system.js.map +0 -1
  47. package/dist/agent/tools/ai-editing.d.ts +0 -15
  48. package/dist/agent/tools/ai-editing.d.ts.map +0 -1
  49. package/dist/agent/tools/ai-editing.js +0 -763
  50. package/dist/agent/tools/ai-editing.js.map +0 -1
  51. package/dist/agent/tools/ai-generation.d.ts +0 -13
  52. package/dist/agent/tools/ai-generation.d.ts.map +0 -1
  53. package/dist/agent/tools/ai-generation.js +0 -973
  54. package/dist/agent/tools/ai-generation.js.map +0 -1
  55. package/dist/agent/tools/ai-pipeline.d.ts +0 -14
  56. package/dist/agent/tools/ai-pipeline.d.ts.map +0 -1
  57. package/dist/agent/tools/ai-pipeline.js +0 -961
  58. package/dist/agent/tools/ai-pipeline.js.map +0 -1
  59. package/dist/agent/tools/ai.d.ts +0 -13
  60. package/dist/agent/tools/ai.d.ts.map +0 -1
  61. package/dist/agent/tools/ai.js +0 -19
  62. package/dist/agent/tools/ai.js.map +0 -1
  63. package/dist/agent/tools/batch.d.ts +0 -6
  64. package/dist/agent/tools/batch.d.ts.map +0 -1
  65. package/dist/agent/tools/batch.js +0 -383
  66. package/dist/agent/tools/batch.js.map +0 -1
  67. package/dist/agent/tools/e2e.test.d.ts +0 -26
  68. package/dist/agent/tools/e2e.test.d.ts.map +0 -1
  69. package/dist/agent/tools/e2e.test.js +0 -397
  70. package/dist/agent/tools/e2e.test.js.map +0 -1
  71. package/dist/agent/tools/export.d.ts +0 -6
  72. package/dist/agent/tools/export.d.ts.map +0 -1
  73. package/dist/agent/tools/export.js +0 -171
  74. package/dist/agent/tools/export.js.map +0 -1
  75. package/dist/agent/tools/filesystem.d.ts +0 -6
  76. package/dist/agent/tools/filesystem.d.ts.map +0 -1
  77. package/dist/agent/tools/filesystem.js +0 -212
  78. package/dist/agent/tools/filesystem.js.map +0 -1
  79. package/dist/agent/tools/index.d.ts +0 -66
  80. package/dist/agent/tools/index.d.ts.map +0 -1
  81. package/dist/agent/tools/index.js +0 -123
  82. package/dist/agent/tools/index.js.map +0 -1
  83. package/dist/agent/tools/integration.test.d.ts +0 -11
  84. package/dist/agent/tools/integration.test.d.ts.map +0 -1
  85. package/dist/agent/tools/integration.test.js +0 -659
  86. package/dist/agent/tools/integration.test.js.map +0 -1
  87. package/dist/agent/tools/media.d.ts +0 -6
  88. package/dist/agent/tools/media.d.ts.map +0 -1
  89. package/dist/agent/tools/media.js +0 -616
  90. package/dist/agent/tools/media.js.map +0 -1
  91. package/dist/agent/tools/project.d.ts +0 -6
  92. package/dist/agent/tools/project.d.ts.map +0 -1
  93. package/dist/agent/tools/project.js +0 -284
  94. package/dist/agent/tools/project.js.map +0 -1
  95. package/dist/agent/tools/scene.d.ts +0 -17
  96. package/dist/agent/tools/scene.d.ts.map +0 -1
  97. package/dist/agent/tools/scene.js +0 -263
  98. package/dist/agent/tools/scene.js.map +0 -1
  99. package/dist/agent/tools/scene.test.d.ts +0 -2
  100. package/dist/agent/tools/scene.test.d.ts.map +0 -1
  101. package/dist/agent/tools/scene.test.js +0 -139
  102. package/dist/agent/tools/scene.test.js.map +0 -1
  103. package/dist/agent/tools/timeline.d.ts +0 -6
  104. package/dist/agent/tools/timeline.d.ts.map +0 -1
  105. package/dist/agent/tools/timeline.js +0 -875
  106. package/dist/agent/tools/timeline.js.map +0 -1
  107. package/dist/agent/types.d.ts +0 -59
  108. package/dist/agent/types.d.ts.map +0 -1
  109. package/dist/agent/types.js +0 -5
  110. package/dist/agent/types.js.map +0 -1
  111. package/dist/commands/_shared/scene-html-emit.d.ts +0 -120
  112. package/dist/commands/_shared/scene-html-emit.d.ts.map +0 -1
  113. package/dist/commands/_shared/scene-html-emit.js +0 -422
  114. package/dist/commands/_shared/scene-html-emit.js.map +0 -1
  115. package/dist/commands/_shared/scene-html-emit.test.d.ts +0 -2
  116. package/dist/commands/_shared/scene-html-emit.test.d.ts.map +0 -1
  117. package/dist/commands/_shared/scene-html-emit.test.js +0 -396
  118. package/dist/commands/_shared/scene-html-emit.test.js.map +0 -1
  119. package/dist/commands/_shared/scene-lint.d.ts +0 -110
  120. package/dist/commands/_shared/scene-lint.d.ts.map +0 -1
  121. package/dist/commands/_shared/scene-lint.js +0 -207
  122. package/dist/commands/_shared/scene-lint.js.map +0 -1
  123. package/dist/commands/_shared/scene-lint.test.d.ts +0 -2
  124. package/dist/commands/_shared/scene-lint.test.d.ts.map +0 -1
  125. package/dist/commands/_shared/scene-lint.test.js +0 -208
  126. package/dist/commands/_shared/scene-lint.test.js.map +0 -1
  127. package/dist/commands/_shared/scene-project.d.ts +0 -101
  128. package/dist/commands/_shared/scene-project.d.ts.map +0 -1
  129. package/dist/commands/_shared/scene-project.js +0 -281
  130. package/dist/commands/_shared/scene-project.js.map +0 -1
  131. package/dist/commands/_shared/scene-project.test.d.ts +0 -2
  132. package/dist/commands/_shared/scene-project.test.d.ts.map +0 -1
  133. package/dist/commands/_shared/scene-project.test.js +0 -203
  134. package/dist/commands/_shared/scene-project.test.js.map +0 -1
  135. package/dist/commands/_shared/scene-render.d.ts +0 -79
  136. package/dist/commands/_shared/scene-render.d.ts.map +0 -1
  137. package/dist/commands/_shared/scene-render.js +0 -138
  138. package/dist/commands/_shared/scene-render.js.map +0 -1
  139. package/dist/commands/_shared/scene-render.test.d.ts +0 -2
  140. package/dist/commands/_shared/scene-render.test.d.ts.map +0 -1
  141. package/dist/commands/_shared/scene-render.test.js +0 -145
  142. package/dist/commands/_shared/scene-render.test.js.map +0 -1
  143. package/dist/commands/_shared/segments-to-scenes.d.ts +0 -85
  144. package/dist/commands/_shared/segments-to-scenes.d.ts.map +0 -1
  145. package/dist/commands/_shared/segments-to-scenes.js +0 -202
  146. package/dist/commands/_shared/segments-to-scenes.js.map +0 -1
  147. package/dist/commands/_shared/segments-to-scenes.test.d.ts +0 -2
  148. package/dist/commands/_shared/segments-to-scenes.test.d.ts.map +0 -1
  149. package/dist/commands/_shared/segments-to-scenes.test.js +0 -219
  150. package/dist/commands/_shared/segments-to-scenes.test.js.map +0 -1
  151. package/dist/commands/_shared/tts-resolve.d.ts +0 -75
  152. package/dist/commands/_shared/tts-resolve.d.ts.map +0 -1
  153. package/dist/commands/_shared/tts-resolve.js +0 -89
  154. package/dist/commands/_shared/tts-resolve.js.map +0 -1
  155. package/dist/commands/_shared/tts-resolve.test.d.ts +0 -6
  156. package/dist/commands/_shared/tts-resolve.test.d.ts.map +0 -1
  157. package/dist/commands/_shared/tts-resolve.test.js +0 -147
  158. package/dist/commands/_shared/tts-resolve.test.js.map +0 -1
  159. package/dist/commands/agent.d.ts +0 -21
  160. package/dist/commands/agent.d.ts.map +0 -1
  161. package/dist/commands/agent.js +0 -285
  162. package/dist/commands/agent.js.map +0 -1
  163. package/dist/commands/ai-analyze.d.ts +0 -106
  164. package/dist/commands/ai-analyze.d.ts.map +0 -1
  165. package/dist/commands/ai-analyze.js +0 -320
  166. package/dist/commands/ai-analyze.js.map +0 -1
  167. package/dist/commands/ai-animated-caption.d.ts +0 -64
  168. package/dist/commands/ai-animated-caption.d.ts.map +0 -1
  169. package/dist/commands/ai-animated-caption.js +0 -272
  170. package/dist/commands/ai-animated-caption.js.map +0 -1
  171. package/dist/commands/ai-audio.d.ts +0 -98
  172. package/dist/commands/ai-audio.d.ts.map +0 -1
  173. package/dist/commands/ai-audio.js +0 -1059
  174. package/dist/commands/ai-audio.js.map +0 -1
  175. package/dist/commands/ai-broll.d.ts +0 -15
  176. package/dist/commands/ai-broll.d.ts.map +0 -1
  177. package/dist/commands/ai-broll.js +0 -421
  178. package/dist/commands/ai-broll.js.map +0 -1
  179. package/dist/commands/ai-edit-cli.d.ts +0 -14
  180. package/dist/commands/ai-edit-cli.d.ts.map +0 -1
  181. package/dist/commands/ai-edit-cli.js +0 -585
  182. package/dist/commands/ai-edit-cli.js.map +0 -1
  183. package/dist/commands/ai-edit.d.ts +0 -398
  184. package/dist/commands/ai-edit.d.ts.map +0 -1
  185. package/dist/commands/ai-edit.js +0 -1060
  186. package/dist/commands/ai-edit.js.map +0 -1
  187. package/dist/commands/ai-fill-gaps.d.ts +0 -14
  188. package/dist/commands/ai-fill-gaps.d.ts.map +0 -1
  189. package/dist/commands/ai-fill-gaps.js +0 -447
  190. package/dist/commands/ai-fill-gaps.js.map +0 -1
  191. package/dist/commands/ai-helpers.d.ts +0 -20
  192. package/dist/commands/ai-helpers.d.ts.map +0 -1
  193. package/dist/commands/ai-helpers.js +0 -59
  194. package/dist/commands/ai-helpers.js.map +0 -1
  195. package/dist/commands/ai-highlights.d.ts +0 -127
  196. package/dist/commands/ai-highlights.d.ts.map +0 -1
  197. package/dist/commands/ai-highlights.js +0 -1053
  198. package/dist/commands/ai-highlights.js.map +0 -1
  199. package/dist/commands/ai-image.d.ts +0 -76
  200. package/dist/commands/ai-image.d.ts.map +0 -1
  201. package/dist/commands/ai-image.js +0 -858
  202. package/dist/commands/ai-image.js.map +0 -1
  203. package/dist/commands/ai-motion.d.ts +0 -50
  204. package/dist/commands/ai-motion.d.ts.map +0 -1
  205. package/dist/commands/ai-motion.js +0 -297
  206. package/dist/commands/ai-motion.js.map +0 -1
  207. package/dist/commands/ai-narrate.d.ts +0 -66
  208. package/dist/commands/ai-narrate.d.ts.map +0 -1
  209. package/dist/commands/ai-narrate.js +0 -332
  210. package/dist/commands/ai-narrate.js.map +0 -1
  211. package/dist/commands/ai-review.d.ts +0 -57
  212. package/dist/commands/ai-review.d.ts.map +0 -1
  213. package/dist/commands/ai-review.js +0 -270
  214. package/dist/commands/ai-review.js.map +0 -1
  215. package/dist/commands/ai-script-pipeline-cli.d.ts +0 -9
  216. package/dist/commands/ai-script-pipeline-cli.d.ts.map +0 -1
  217. package/dist/commands/ai-script-pipeline-cli.js +0 -502
  218. package/dist/commands/ai-script-pipeline-cli.js.map +0 -1
  219. package/dist/commands/ai-script-pipeline.d.ts +0 -305
  220. package/dist/commands/ai-script-pipeline.d.ts.map +0 -1
  221. package/dist/commands/ai-script-pipeline.js +0 -1546
  222. package/dist/commands/ai-script-pipeline.js.map +0 -1
  223. package/dist/commands/ai-suggest-edit.d.ts +0 -14
  224. package/dist/commands/ai-suggest-edit.d.ts.map +0 -1
  225. package/dist/commands/ai-suggest-edit.js +0 -215
  226. package/dist/commands/ai-suggest-edit.js.map +0 -1
  227. package/dist/commands/ai-video-fx.d.ts +0 -14
  228. package/dist/commands/ai-video-fx.d.ts.map +0 -1
  229. package/dist/commands/ai-video-fx.js +0 -396
  230. package/dist/commands/ai-video-fx.js.map +0 -1
  231. package/dist/commands/ai-video.d.ts +0 -91
  232. package/dist/commands/ai-video.d.ts.map +0 -1
  233. package/dist/commands/ai-video.js +0 -1055
  234. package/dist/commands/ai-video.js.map +0 -1
  235. package/dist/commands/ai-viral.d.ts +0 -15
  236. package/dist/commands/ai-viral.d.ts.map +0 -1
  237. package/dist/commands/ai-viral.js +0 -507
  238. package/dist/commands/ai-viral.js.map +0 -1
  239. package/dist/commands/ai-visual-fx.d.ts +0 -14
  240. package/dist/commands/ai-visual-fx.d.ts.map +0 -1
  241. package/dist/commands/ai-visual-fx.js +0 -521
  242. package/dist/commands/ai-visual-fx.js.map +0 -1
  243. package/dist/commands/ai.d.ts +0 -38
  244. package/dist/commands/ai.d.ts.map +0 -1
  245. package/dist/commands/ai.js +0 -225
  246. package/dist/commands/ai.js.map +0 -1
  247. package/dist/commands/ai.test.d.ts +0 -2
  248. package/dist/commands/ai.test.d.ts.map +0 -1
  249. package/dist/commands/ai.test.js +0 -557
  250. package/dist/commands/ai.test.js.map +0 -1
  251. package/dist/commands/analyze.d.ts +0 -16
  252. package/dist/commands/analyze.d.ts.map +0 -1
  253. package/dist/commands/analyze.js +0 -247
  254. package/dist/commands/analyze.js.map +0 -1
  255. package/dist/commands/audio.d.ts +0 -18
  256. package/dist/commands/audio.d.ts.map +0 -1
  257. package/dist/commands/audio.js +0 -539
  258. package/dist/commands/audio.js.map +0 -1
  259. package/dist/commands/batch.d.ts +0 -3
  260. package/dist/commands/batch.d.ts.map +0 -1
  261. package/dist/commands/batch.js +0 -419
  262. package/dist/commands/batch.js.map +0 -1
  263. package/dist/commands/batch.test.d.ts +0 -2
  264. package/dist/commands/batch.test.d.ts.map +0 -1
  265. package/dist/commands/batch.test.js +0 -203
  266. package/dist/commands/batch.test.js.map +0 -1
  267. package/dist/commands/context.d.ts +0 -7
  268. package/dist/commands/context.d.ts.map +0 -1
  269. package/dist/commands/context.js +0 -66
  270. package/dist/commands/context.js.map +0 -1
  271. package/dist/commands/demo.d.ts +0 -7
  272. package/dist/commands/demo.d.ts.map +0 -1
  273. package/dist/commands/demo.js +0 -192
  274. package/dist/commands/demo.js.map +0 -1
  275. package/dist/commands/detect.d.ts +0 -56
  276. package/dist/commands/detect.d.ts.map +0 -1
  277. package/dist/commands/detect.js +0 -464
  278. package/dist/commands/detect.js.map +0 -1
  279. package/dist/commands/doctor.d.ts +0 -6
  280. package/dist/commands/doctor.d.ts.map +0 -1
  281. package/dist/commands/doctor.js +0 -322
  282. package/dist/commands/doctor.js.map +0 -1
  283. package/dist/commands/edit-cmd.d.ts +0 -112
  284. package/dist/commands/edit-cmd.d.ts.map +0 -1
  285. package/dist/commands/edit-cmd.js +0 -1041
  286. package/dist/commands/edit-cmd.js.map +0 -1
  287. package/dist/commands/export.d.ts +0 -61
  288. package/dist/commands/export.d.ts.map +0 -1
  289. package/dist/commands/export.js +0 -980
  290. package/dist/commands/export.js.map +0 -1
  291. package/dist/commands/export.test.d.ts +0 -2
  292. package/dist/commands/export.test.d.ts.map +0 -1
  293. package/dist/commands/export.test.js +0 -107
  294. package/dist/commands/export.test.js.map +0 -1
  295. package/dist/commands/generate.d.ts +0 -84
  296. package/dist/commands/generate.d.ts.map +0 -1
  297. package/dist/commands/generate.js +0 -2030
  298. package/dist/commands/generate.js.map +0 -1
  299. package/dist/commands/media.d.ts +0 -3
  300. package/dist/commands/media.d.ts.map +0 -1
  301. package/dist/commands/media.js +0 -163
  302. package/dist/commands/media.js.map +0 -1
  303. package/dist/commands/output.d.ts +0 -51
  304. package/dist/commands/output.d.ts.map +0 -1
  305. package/dist/commands/output.js +0 -219
  306. package/dist/commands/output.js.map +0 -1
  307. package/dist/commands/output.test.d.ts +0 -2
  308. package/dist/commands/output.test.d.ts.map +0 -1
  309. package/dist/commands/output.test.js +0 -151
  310. package/dist/commands/output.test.js.map +0 -1
  311. package/dist/commands/pipeline.d.ts +0 -19
  312. package/dist/commands/pipeline.d.ts.map +0 -1
  313. package/dist/commands/pipeline.js +0 -345
  314. package/dist/commands/pipeline.js.map +0 -1
  315. package/dist/commands/project.d.ts +0 -3
  316. package/dist/commands/project.d.ts.map +0 -1
  317. package/dist/commands/project.js +0 -181
  318. package/dist/commands/project.js.map +0 -1
  319. package/dist/commands/project.test.d.ts +0 -2
  320. package/dist/commands/project.test.d.ts.map +0 -1
  321. package/dist/commands/project.test.js +0 -105
  322. package/dist/commands/project.test.js.map +0 -1
  323. package/dist/commands/run.d.ts +0 -15
  324. package/dist/commands/run.d.ts.map +0 -1
  325. package/dist/commands/run.js +0 -204
  326. package/dist/commands/run.js.map +0 -1
  327. package/dist/commands/sanitize.d.ts +0 -21
  328. package/dist/commands/sanitize.d.ts.map +0 -1
  329. package/dist/commands/sanitize.js +0 -56
  330. package/dist/commands/sanitize.js.map +0 -1
  331. package/dist/commands/scene.d.ts +0 -86
  332. package/dist/commands/scene.d.ts.map +0 -1
  333. package/dist/commands/scene.js +0 -719
  334. package/dist/commands/scene.js.map +0 -1
  335. package/dist/commands/scene.test.d.ts +0 -2
  336. package/dist/commands/scene.test.d.ts.map +0 -1
  337. package/dist/commands/scene.test.js +0 -219
  338. package/dist/commands/scene.test.js.map +0 -1
  339. package/dist/commands/schema.d.ts +0 -13
  340. package/dist/commands/schema.d.ts.map +0 -1
  341. package/dist/commands/schema.js +0 -218
  342. package/dist/commands/schema.js.map +0 -1
  343. package/dist/commands/setup.d.ts +0 -6
  344. package/dist/commands/setup.d.ts.map +0 -1
  345. package/dist/commands/setup.js +0 -461
  346. package/dist/commands/setup.js.map +0 -1
  347. package/dist/commands/timeline.d.ts +0 -3
  348. package/dist/commands/timeline.d.ts.map +0 -1
  349. package/dist/commands/timeline.js +0 -609
  350. package/dist/commands/timeline.js.map +0 -1
  351. package/dist/commands/timeline.test.d.ts +0 -2
  352. package/dist/commands/timeline.test.d.ts.map +0 -1
  353. package/dist/commands/timeline.test.js +0 -334
  354. package/dist/commands/timeline.test.js.map +0 -1
  355. package/dist/commands/validate.d.ts +0 -34
  356. package/dist/commands/validate.d.ts.map +0 -1
  357. package/dist/commands/validate.js +0 -70
  358. package/dist/commands/validate.js.map +0 -1
  359. package/dist/config/config.test.d.ts +0 -2
  360. package/dist/config/config.test.d.ts.map +0 -1
  361. package/dist/config/config.test.js +0 -164
  362. package/dist/config/config.test.js.map +0 -1
  363. package/dist/config/index.d.ts +0 -35
  364. package/dist/config/index.d.ts.map +0 -1
  365. package/dist/config/index.js +0 -101
  366. package/dist/config/index.js.map +0 -1
  367. package/dist/config/schema.d.ts +0 -52
  368. package/dist/config/schema.d.ts.map +0 -1
  369. package/dist/config/schema.js +0 -44
  370. package/dist/config/schema.js.map +0 -1
  371. package/dist/engine/index.d.ts +0 -3
  372. package/dist/engine/index.d.ts.map +0 -1
  373. package/dist/engine/index.js +0 -2
  374. package/dist/engine/index.js.map +0 -1
  375. package/dist/engine/project.d.ts +0 -84
  376. package/dist/engine/project.d.ts.map +0 -1
  377. package/dist/engine/project.js +0 -355
  378. package/dist/engine/project.js.map +0 -1
  379. package/dist/engine/project.test.d.ts +0 -2
  380. package/dist/engine/project.test.d.ts.map +0 -1
  381. package/dist/engine/project.test.js +0 -599
  382. package/dist/engine/project.test.js.map +0 -1
  383. package/dist/index.d.ts +0 -7
  384. package/dist/index.d.ts.map +0 -1
  385. package/dist/index.js.map +0 -1
  386. package/dist/pipeline/executor.d.ts +0 -19
  387. package/dist/pipeline/executor.d.ts.map +0 -1
  388. package/dist/pipeline/executor.js +0 -397
  389. package/dist/pipeline/executor.js.map +0 -1
  390. package/dist/pipeline/index.d.ts +0 -4
  391. package/dist/pipeline/index.d.ts.map +0 -1
  392. package/dist/pipeline/index.js +0 -2
  393. package/dist/pipeline/index.js.map +0 -1
  394. package/dist/pipeline/renderers/__tests__/html-template.test.d.ts +0 -2
  395. package/dist/pipeline/renderers/__tests__/html-template.test.d.ts.map +0 -1
  396. package/dist/pipeline/renderers/__tests__/html-template.test.js +0 -170
  397. package/dist/pipeline/renderers/__tests__/html-template.test.js.map +0 -1
  398. package/dist/pipeline/renderers/__tests__/integration.test.d.ts +0 -2
  399. package/dist/pipeline/renderers/__tests__/integration.test.d.ts.map +0 -1
  400. package/dist/pipeline/renderers/__tests__/integration.test.js +0 -175
  401. package/dist/pipeline/renderers/__tests__/integration.test.js.map +0 -1
  402. package/dist/pipeline/renderers/chrome.d.ts +0 -26
  403. package/dist/pipeline/renderers/chrome.d.ts.map +0 -1
  404. package/dist/pipeline/renderers/chrome.js +0 -48
  405. package/dist/pipeline/renderers/chrome.js.map +0 -1
  406. package/dist/pipeline/renderers/html-clips.d.ts +0 -20
  407. package/dist/pipeline/renderers/html-clips.d.ts.map +0 -1
  408. package/dist/pipeline/renderers/html-clips.js +0 -50
  409. package/dist/pipeline/renderers/html-clips.js.map +0 -1
  410. package/dist/pipeline/renderers/html-runtime.d.ts +0 -7
  411. package/dist/pipeline/renderers/html-runtime.d.ts.map +0 -1
  412. package/dist/pipeline/renderers/html-runtime.js +0 -80
  413. package/dist/pipeline/renderers/html-runtime.js.map +0 -1
  414. package/dist/pipeline/renderers/html-template.d.ts +0 -3
  415. package/dist/pipeline/renderers/html-template.d.ts.map +0 -1
  416. package/dist/pipeline/renderers/html-template.js +0 -36
  417. package/dist/pipeline/renderers/html-template.js.map +0 -1
  418. package/dist/pipeline/renderers/hyperframes.d.ts +0 -8
  419. package/dist/pipeline/renderers/hyperframes.d.ts.map +0 -1
  420. package/dist/pipeline/renderers/hyperframes.js +0 -63
  421. package/dist/pipeline/renderers/hyperframes.js.map +0 -1
  422. package/dist/pipeline/renderers/project-builder.d.ts +0 -8
  423. package/dist/pipeline/renderers/project-builder.d.ts.map +0 -1
  424. package/dist/pipeline/renderers/project-builder.js +0 -65
  425. package/dist/pipeline/renderers/project-builder.js.map +0 -1
  426. package/dist/pipeline/renderers/types.d.ts +0 -41
  427. package/dist/pipeline/renderers/types.d.ts.map +0 -1
  428. package/dist/pipeline/renderers/types.js +0 -11
  429. package/dist/pipeline/renderers/types.js.map +0 -1
  430. package/dist/pipeline/resolver.d.ts +0 -20
  431. package/dist/pipeline/resolver.d.ts.map +0 -1
  432. package/dist/pipeline/resolver.js +0 -74
  433. package/dist/pipeline/resolver.js.map +0 -1
  434. package/dist/pipeline/types.d.ts +0 -82
  435. package/dist/pipeline/types.d.ts.map +0 -1
  436. package/dist/pipeline/types.js +0 -8
  437. package/dist/pipeline/types.js.map +0 -1
  438. package/dist/utils/api-key.d.ts +0 -33
  439. package/dist/utils/api-key.d.ts.map +0 -1
  440. package/dist/utils/api-key.js +0 -228
  441. package/dist/utils/api-key.js.map +0 -1
  442. package/dist/utils/api-key.test.d.ts +0 -2
  443. package/dist/utils/api-key.test.d.ts.map +0 -1
  444. package/dist/utils/api-key.test.js +0 -35
  445. package/dist/utils/api-key.test.js.map +0 -1
  446. package/dist/utils/audio.d.ts +0 -23
  447. package/dist/utils/audio.d.ts.map +0 -1
  448. package/dist/utils/audio.js +0 -79
  449. package/dist/utils/audio.js.map +0 -1
  450. package/dist/utils/exec-safe.d.ts +0 -22
  451. package/dist/utils/exec-safe.d.ts.map +0 -1
  452. package/dist/utils/exec-safe.js +0 -62
  453. package/dist/utils/exec-safe.js.map +0 -1
  454. package/dist/utils/first-run.d.ts +0 -17
  455. package/dist/utils/first-run.d.ts.map +0 -1
  456. package/dist/utils/first-run.js +0 -73
  457. package/dist/utils/first-run.js.map +0 -1
  458. package/dist/utils/input-validation.d.ts +0 -27
  459. package/dist/utils/input-validation.d.ts.map +0 -1
  460. package/dist/utils/input-validation.js +0 -67
  461. package/dist/utils/input-validation.js.map +0 -1
  462. package/dist/utils/provider-resolver.d.ts +0 -19
  463. package/dist/utils/provider-resolver.d.ts.map +0 -1
  464. package/dist/utils/provider-resolver.js +0 -75
  465. package/dist/utils/provider-resolver.js.map +0 -1
  466. package/dist/utils/remotion.d.ts +0 -231
  467. package/dist/utils/remotion.d.ts.map +0 -1
  468. package/dist/utils/remotion.js +0 -879
  469. package/dist/utils/remotion.js.map +0 -1
  470. package/dist/utils/subtitle.d.ts +0 -65
  471. package/dist/utils/subtitle.d.ts.map +0 -1
  472. package/dist/utils/subtitle.js +0 -135
  473. package/dist/utils/subtitle.js.map +0 -1
  474. package/dist/utils/subtitle.test.d.ts +0 -2
  475. package/dist/utils/subtitle.test.d.ts.map +0 -1
  476. package/dist/utils/subtitle.test.js +0 -175
  477. package/dist/utils/subtitle.test.js.map +0 -1
  478. package/dist/utils/tty.d.ts +0 -45
  479. package/dist/utils/tty.d.ts.map +0 -1
  480. package/dist/utils/tty.js +0 -231
  481. package/dist/utils/tty.js.map +0 -1
@@ -1,1053 +0,0 @@
1
- /**
2
- * @module ai-highlights
3
- *
4
- * Highlight extraction and auto-shorts generation for long-form content.
5
- *
6
- * CLI commands: highlights, auto-shorts
7
- *
8
- * Execute functions:
9
- * executeHighlights - Extract best moments from video/audio (Whisper+Claude or Gemini)
10
- * executeAutoShorts - Generate short-form vertical clips from long-form video
11
- *
12
- * @dependencies Whisper (OpenAI), Claude (Anthropic), Gemini (Google), FFmpeg
13
- */
14
- import { readFile, writeFile, mkdir } from "node:fs/promises";
15
- import { resolve, dirname, basename, extname } from "node:path";
16
- import { existsSync } from "node:fs";
17
- import chalk from "chalk";
18
- import ora from "ora";
19
- import { GeminiProvider, WhisperProvider, ClaudeProvider, } from "@vibeframe/ai-providers";
20
- import { Project } from "../engine/index.js";
21
- import { getApiKey } from "../utils/api-key.js";
22
- import { formatTime } from "./ai-helpers.js";
23
- import { execSafe, commandExists, ffprobeDuration } from "../utils/exec-safe.js";
24
- import { exitWithError, outputResult, authError, notFoundError, apiError, generalError } from "./output.js";
25
- import { validateOutputPath } from "./validate.js";
26
- // ============================================================================
27
- // Shared helpers
28
- // ============================================================================
29
- function filterHighlights(highlights, options) {
30
- let filtered = highlights.filter((h) => h.confidence >= options.threshold);
31
- filtered.sort((a, b) => b.confidence - a.confidence);
32
- if (options.maxCount && filtered.length > options.maxCount) {
33
- filtered = filtered.slice(0, options.maxCount);
34
- }
35
- if (options.targetDuration) {
36
- const targetWithTolerance = options.targetDuration * 1.1;
37
- let total = 0;
38
- filtered = filtered.filter((h) => {
39
- if (total + h.duration <= targetWithTolerance) {
40
- total += h.duration;
41
- return true;
42
- }
43
- return false;
44
- });
45
- }
46
- filtered.sort((a, b) => a.startTime - b.startTime);
47
- return filtered.map((h, i) => ({ ...h, index: i + 1 }));
48
- }
49
- function getCategoryColor(category) {
50
- switch (category) {
51
- case "emotional":
52
- return chalk.magenta;
53
- case "informative":
54
- return chalk.cyan;
55
- case "funny":
56
- return chalk.yellow;
57
- default:
58
- return chalk.white;
59
- }
60
- }
61
- function truncate(text, maxLength) {
62
- if (text.length <= maxLength)
63
- return text;
64
- return text.slice(0, maxLength - 3) + "...";
65
- }
66
- /**
67
- * Extract the best highlights from a video or audio file.
68
- *
69
- * Supports two analysis backends:
70
- * - Whisper + Claude (default): transcribe audio, then analyze text for highlights
71
- * - Gemini (--useGemini): multimodal visual+audio analysis for richer detection
72
- *
73
- * @param options - Highlight extraction configuration
74
- * @returns Result with filtered highlights sorted by time
75
- */
76
- export async function executeHighlights(options) {
77
- try {
78
- const absPath = resolve(process.cwd(), options.media);
79
- if (!existsSync(absPath)) {
80
- return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: `File not found: ${absPath}` };
81
- }
82
- const ext = extname(absPath).toLowerCase();
83
- const videoExtensions = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
84
- const isVideo = videoExtensions.includes(ext);
85
- const targetDuration = options.duration;
86
- const maxCount = options.count;
87
- const threshold = options.threshold ?? 0.7;
88
- let allHighlights = [];
89
- let sourceDuration = 0;
90
- if (options.useGemini && isVideo) {
91
- const geminiApiKey = await getApiKey("GOOGLE_API_KEY", "Google");
92
- if (!geminiApiKey) {
93
- return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: "Google API key required for Gemini Video Understanding. Run 'vibe setup' or set GOOGLE_API_KEY in .env" };
94
- }
95
- sourceDuration = await ffprobeDuration(absPath);
96
- const gemini = new GeminiProvider();
97
- await gemini.initialize({ apiKey: geminiApiKey });
98
- const videoBuffer = await readFile(absPath);
99
- const criteriaText = options.criteria === "all" || !options.criteria
100
- ? "emotional, informative, and funny moments"
101
- : `${options.criteria} moments`;
102
- const durationText = targetDuration ? `Target a total highlight duration of ${targetDuration} seconds.` : "";
103
- const countText = maxCount ? `Find up to ${maxCount} highlights.` : "";
104
- const geminiPrompt = `Analyze this video and identify the most engaging highlights based on BOTH visual and audio content.
105
-
106
- Focus on finding ${criteriaText}. ${durationText} ${countText}
107
-
108
- For each highlight, provide:
109
- 1. Start timestamp (in seconds, as a number)
110
- 2. End timestamp (in seconds, as a number)
111
- 3. Category: "emotional", "informative", or "funny"
112
- 4. Confidence score (0-1)
113
- 5. Brief reason why this is a highlight
114
- 6. What is said/shown during this moment
115
-
116
- IMPORTANT: Respond ONLY with valid JSON in this exact format:
117
- {
118
- "highlights": [
119
- {
120
- "startTime": 12.5,
121
- "endTime": 28.3,
122
- "category": "emotional",
123
- "confidence": 0.95,
124
- "reason": "Powerful personal story about overcoming challenges",
125
- "transcript": "When I first started, everyone said it was impossible..."
126
- }
127
- ]
128
- }
129
-
130
- Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID (speech, reactions) to find the most compelling moments.`;
131
- const result = await gemini.analyzeVideo(videoBuffer, geminiPrompt, {
132
- fps: 1,
133
- lowResolution: options.lowRes,
134
- });
135
- if (!result.success || !result.response) {
136
- return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: `Gemini analysis failed: ${result.error}` };
137
- }
138
- try {
139
- let jsonStr = result.response;
140
- const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
141
- if (jsonMatch)
142
- jsonStr = jsonMatch[1];
143
- const objectMatch = jsonStr.match(/\{[\s\S]*"highlights"[\s\S]*\}/);
144
- if (objectMatch)
145
- jsonStr = objectMatch[0];
146
- const parsed = JSON.parse(jsonStr);
147
- if (parsed.highlights && Array.isArray(parsed.highlights)) {
148
- allHighlights = parsed.highlights.map((h, i) => ({
149
- index: i + 1,
150
- startTime: h.startTime,
151
- endTime: h.endTime,
152
- duration: h.endTime - h.startTime,
153
- category: h.category || "all",
154
- confidence: h.confidence || 0.8,
155
- reason: h.reason || "Engaging moment",
156
- transcript: h.transcript || "",
157
- }));
158
- }
159
- }
160
- catch {
161
- return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: "Failed to parse Gemini response" };
162
- }
163
- }
164
- else {
165
- const openaiApiKey = await getApiKey("OPENAI_API_KEY", "OpenAI");
166
- if (!openaiApiKey) {
167
- return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: "OpenAI API key required for Whisper transcription. Run 'vibe setup' or set OPENAI_API_KEY in .env" };
168
- }
169
- const claudeApiKey = await getApiKey("ANTHROPIC_API_KEY", "Anthropic");
170
- if (!claudeApiKey) {
171
- return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: "Anthropic API key required for highlight analysis. Run 'vibe setup' or set ANTHROPIC_API_KEY in .env" };
172
- }
173
- let audioPath = absPath;
174
- let tempAudioPath = null;
175
- if (isVideo) {
176
- if (!commandExists("ffmpeg")) {
177
- return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: "FFmpeg not found" };
178
- }
179
- tempAudioPath = `/tmp/vibe_highlight_audio_${Date.now()}.wav`;
180
- await execSafe("ffmpeg", [
181
- "-i", absPath, "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", tempAudioPath, "-y",
182
- ], { maxBuffer: 50 * 1024 * 1024 });
183
- audioPath = tempAudioPath;
184
- sourceDuration = await ffprobeDuration(absPath);
185
- }
186
- const whisper = new WhisperProvider();
187
- await whisper.initialize({ apiKey: openaiApiKey });
188
- const audioBuffer = await readFile(audioPath);
189
- const audioBlob = new Blob([audioBuffer]);
190
- const transcriptResult = await whisper.transcribe(audioBlob, options.language);
191
- if (tempAudioPath && existsSync(tempAudioPath)) {
192
- const { unlink: unlinkFile } = await import("node:fs/promises");
193
- await unlinkFile(tempAudioPath).catch(() => { });
194
- }
195
- if (transcriptResult.status === "failed" || !transcriptResult.segments) {
196
- return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: `Transcription failed: ${transcriptResult.error}` };
197
- }
198
- if (transcriptResult.segments.length > 0) {
199
- sourceDuration = transcriptResult.segments[transcriptResult.segments.length - 1].endTime;
200
- }
201
- const claude = new ClaudeProvider();
202
- await claude.initialize({ apiKey: claudeApiKey });
203
- allHighlights = await claude.analyzeForHighlights(transcriptResult.segments, {
204
- criteria: (options.criteria || "all"),
205
- targetDuration,
206
- maxCount,
207
- });
208
- }
209
- if (allHighlights.length === 0) {
210
- return { success: true, highlights: [], totalDuration: sourceDuration, totalHighlightDuration: 0 };
211
- }
212
- const filteredHighlights = filterHighlights(allHighlights, { threshold, targetDuration, maxCount });
213
- const totalHighlightDuration = filteredHighlights.reduce((sum, h) => sum + h.duration, 0);
214
- const extractResult = {
215
- success: true,
216
- highlights: filteredHighlights,
217
- totalDuration: sourceDuration,
218
- totalHighlightDuration,
219
- };
220
- if (options.output) {
221
- const outputPath = resolve(process.cwd(), options.output);
222
- await writeFile(outputPath, JSON.stringify({
223
- sourceFile: absPath,
224
- totalDuration: sourceDuration,
225
- criteria: options.criteria || "all",
226
- threshold,
227
- highlightsCount: filteredHighlights.length,
228
- totalHighlightDuration,
229
- highlights: filteredHighlights,
230
- }, null, 2), "utf-8");
231
- extractResult.outputPath = outputPath;
232
- }
233
- if (options.project) {
234
- const project = new Project("Highlight Reel");
235
- const source = project.addSource({
236
- name: basename(absPath),
237
- url: absPath,
238
- type: isVideo ? "video" : "audio",
239
- duration: sourceDuration,
240
- });
241
- const videoTrack = project.getTracks().find((t) => t.type === "video");
242
- if (videoTrack) {
243
- let currentTime = 0;
244
- for (const highlight of filteredHighlights) {
245
- project.addClip({
246
- sourceId: source.id,
247
- trackId: videoTrack.id,
248
- startTime: currentTime,
249
- duration: highlight.duration,
250
- sourceStartOffset: highlight.startTime,
251
- sourceEndOffset: highlight.endTime,
252
- });
253
- currentTime += highlight.duration;
254
- }
255
- }
256
- const projectPath = resolve(process.cwd(), options.project);
257
- await writeFile(projectPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
258
- extractResult.projectPath = projectPath;
259
- }
260
- return extractResult;
261
- }
262
- catch (error) {
263
- return {
264
- success: false,
265
- highlights: [],
266
- totalDuration: 0,
267
- totalHighlightDuration: 0,
268
- error: error instanceof Error ? error.message : String(error),
269
- };
270
- }
271
- }
272
- /**
273
- * Auto-generate short-form vertical clips from a long-form video.
274
- *
275
- * Finds the best viral-worthy moments using Whisper+Claude or Gemini analysis,
276
- * then extracts and crops them to the target aspect ratio using FFmpeg.
277
- *
278
- * @param options - Auto-shorts generation configuration
279
- * @returns Result with extracted short clips and metadata
280
- */
281
- export async function executeAutoShorts(options) {
282
- try {
283
- if (!commandExists("ffmpeg")) {
284
- return { success: false, shorts: [], error: "FFmpeg not found" };
285
- }
286
- const absPath = resolve(process.cwd(), options.video);
287
- if (!existsSync(absPath)) {
288
- return { success: false, shorts: [], error: `File not found: ${absPath}` };
289
- }
290
- const targetDuration = options.duration ?? 60;
291
- const shortCount = options.count ?? 1;
292
- let highlights = [];
293
- if (options.useGemini) {
294
- const geminiApiKey = await getApiKey("GOOGLE_API_KEY", "Google");
295
- if (!geminiApiKey) {
296
- return { success: false, shorts: [], error: "Google API key required for Gemini Video Understanding. Run 'vibe setup' or set GOOGLE_API_KEY in .env" };
297
- }
298
- const gemini = new GeminiProvider();
299
- await gemini.initialize({ apiKey: geminiApiKey });
300
- const videoBuffer = await readFile(absPath);
301
- const geminiPrompt = `Analyze this video to find the BEST moments for short-form vertical video content (TikTok, YouTube Shorts, Instagram Reels).
302
-
303
- Find ${shortCount * 3} potential clips that are ${targetDuration} seconds or shorter each.
304
-
305
- Look for:
306
- - Visually striking or surprising moments
307
- - Emotional peaks (laughter, reactions, reveals)
308
- - Key quotes or memorable statements
309
- - Action sequences or dramatic moments
310
- - Meme-worthy or shareable moments
311
- - Strong hooks (great opening lines)
312
- - Satisfying conclusions
313
-
314
- For each highlight, provide:
315
- 1. Start timestamp (seconds, as number)
316
- 2. End timestamp (seconds, as number) - ensure duration is close to ${targetDuration}s
317
- 3. Virality score (0-1) - how likely this would perform on social media
318
- 4. Hook quality (0-1) - how strong is the opening
319
- 5. Brief reason why this would work as a short
320
-
321
- IMPORTANT: Respond ONLY with valid JSON:
322
- {
323
- "highlights": [
324
- {
325
- "startTime": 45.2,
326
- "endTime": 75.8,
327
- "confidence": 0.92,
328
- "hookQuality": 0.85,
329
- "reason": "Unexpected plot twist with strong visual reaction"
330
- }
331
- ]
332
- }
333
-
334
- Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, reactions, music) to find viral-worthy moments.`;
335
- const result = await gemini.analyzeVideo(videoBuffer, geminiPrompt, {
336
- fps: 1,
337
- lowResolution: options.lowRes,
338
- });
339
- if (!result.success || !result.response) {
340
- return { success: false, shorts: [], error: `Gemini analysis failed: ${result.error}` };
341
- }
342
- try {
343
- let jsonStr = result.response;
344
- const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
345
- if (jsonMatch)
346
- jsonStr = jsonMatch[1];
347
- const objectMatch = jsonStr.match(/\{[\s\S]*"highlights"[\s\S]*\}/);
348
- if (objectMatch)
349
- jsonStr = objectMatch[0];
350
- const parsed = JSON.parse(jsonStr);
351
- if (parsed.highlights && Array.isArray(parsed.highlights)) {
352
- highlights = parsed.highlights.map((h, i) => ({
353
- index: i + 1,
354
- startTime: h.startTime,
355
- endTime: h.endTime,
356
- duration: h.endTime - h.startTime,
357
- category: "viral",
358
- confidence: h.confidence || 0.8,
359
- reason: h.reason || "Engaging moment",
360
- transcript: "",
361
- }));
362
- }
363
- }
364
- catch {
365
- return { success: false, shorts: [], error: "Failed to parse Gemini response" };
366
- }
367
- }
368
- else {
369
- const openaiApiKey = await getApiKey("OPENAI_API_KEY", "OpenAI");
370
- if (!openaiApiKey) {
371
- return { success: false, shorts: [], error: "OpenAI API key required for transcription. Run 'vibe setup' or set OPENAI_API_KEY in .env" };
372
- }
373
- const claudeApiKey = await getApiKey("ANTHROPIC_API_KEY", "Anthropic");
374
- if (!claudeApiKey) {
375
- return { success: false, shorts: [], error: "Anthropic API key required for highlight detection. Run 'vibe setup' or set ANTHROPIC_API_KEY in .env" };
376
- }
377
- const tempAudio = absPath.replace(/(\.[^.]+)$/, "-temp-audio.mp3");
378
- await execSafe("ffmpeg", ["-i", absPath, "-vn", "-acodec", "libmp3lame", "-q:a", "2", tempAudio, "-y"]);
379
- const whisper = new WhisperProvider();
380
- await whisper.initialize({ apiKey: openaiApiKey });
381
- const audioBuffer = await readFile(tempAudio);
382
- const audioBlob = new Blob([audioBuffer]);
383
- const transcript = await whisper.transcribe(audioBlob, options.language);
384
- try {
385
- const { unlink: unlinkFile } = await import("node:fs/promises");
386
- await unlinkFile(tempAudio);
387
- }
388
- catch { /* ignore */ }
389
- if (!transcript.segments || transcript.segments.length === 0) {
390
- return { success: false, shorts: [], error: "No transcript found" };
391
- }
392
- const claude = new ClaudeProvider();
393
- await claude.initialize({ apiKey: claudeApiKey });
394
- highlights = await claude.analyzeForHighlights(transcript.segments, {
395
- criteria: "all",
396
- targetDuration: targetDuration * shortCount,
397
- maxCount: shortCount * 3,
398
- });
399
- }
400
- if (highlights.length === 0) {
401
- return { success: false, shorts: [], error: "No highlights found" };
402
- }
403
- highlights.sort((a, b) => b.confidence - a.confidence);
404
- const selectedHighlights = highlights.slice(0, shortCount);
405
- if (options.analyzeOnly) {
406
- return {
407
- success: true,
408
- shorts: selectedHighlights.map((h, i) => ({
409
- index: i + 1,
410
- startTime: h.startTime,
411
- endTime: h.endTime,
412
- duration: h.duration,
413
- confidence: h.confidence,
414
- reason: h.reason,
415
- })),
416
- };
417
- }
418
- const outputDir = options.outputDir
419
- ? resolve(process.cwd(), options.outputDir)
420
- : dirname(absPath);
421
- if (options.outputDir && !existsSync(outputDir)) {
422
- await mkdir(outputDir, { recursive: true });
423
- }
424
- const result = {
425
- success: true,
426
- shorts: [],
427
- };
428
- for (let i = 0; i < selectedHighlights.length; i++) {
429
- const h = selectedHighlights[i];
430
- const baseName = basename(absPath, extname(absPath));
431
- const outputPath = resolve(outputDir, `${baseName}-short-${i + 1}.mp4`);
432
- const { stdout: probeOut } = await execSafe("ffprobe", [
433
- "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=p=0", absPath,
434
- ]);
435
- const [width, height] = probeOut.trim().split(",").map(Number);
436
- const aspect = options.aspect || "9:16";
437
- const [targetW, targetH] = aspect.split(":").map(Number);
438
- const targetRatio = targetW / targetH;
439
- const sourceRatio = width / height;
440
- let cropW, cropH, cropX, cropY;
441
- if (sourceRatio > targetRatio) {
442
- cropH = height;
443
- cropW = Math.round(height * targetRatio);
444
- cropX = Math.round((width - cropW) / 2);
445
- cropY = 0;
446
- }
447
- else {
448
- cropW = width;
449
- cropH = Math.round(width / targetRatio);
450
- cropX = 0;
451
- cropY = Math.round((height - cropH) / 2);
452
- }
453
- const vf = `crop=${cropW}:${cropH}:${cropX}:${cropY}`;
454
- await execSafe("ffmpeg", [
455
- "-ss", String(h.startTime), "-i", absPath, "-t", String(h.duration),
456
- "-vf", vf, "-c:a", "aac", "-b:a", "128k", outputPath, "-y",
457
- ], { timeout: 300000 });
458
- result.shorts.push({
459
- index: i + 1,
460
- startTime: h.startTime,
461
- endTime: h.endTime,
462
- duration: h.duration,
463
- confidence: h.confidence,
464
- reason: h.reason,
465
- outputPath,
466
- });
467
- }
468
- return result;
469
- }
470
- catch (error) {
471
- return {
472
- success: false,
473
- shorts: [],
474
- error: error instanceof Error ? error.message : String(error),
475
- };
476
- }
477
- }
478
- // ============================================================================
479
- // CLI command registration
480
- // ============================================================================
481
- export function registerHighlightsCommands(aiCommand) {
482
- aiCommand
483
- .command("highlights")
484
- .description("Extract highlights from long-form video/audio content")
485
- .argument("<media>", "Video or audio file path")
486
- .option("-o, --output <path>", "Output JSON file with highlights")
487
- .option("--project <path>", "Create project with highlight clips")
488
- .option("-d, --duration <seconds>", "Target highlight reel duration", "60")
489
- .option("-n, --count <number>", "Maximum number of highlights")
490
- .option("-t, --threshold <value>", "Confidence threshold (0-1)", "0.7")
491
- .option("--criteria <type>", "Selection criteria: emotional | informative | funny | all", "all")
492
- .option("-l, --language <lang>", "Language code for transcription (e.g., en, ko)")
493
- .option("--use-gemini", "Use Gemini Video Understanding for enhanced visual+audio analysis")
494
- .option("--low-res", "Use low resolution mode for longer videos (Gemini only)")
495
- .option("--dry-run", "Preview parameters without executing")
496
- .action(async (mediaPath, options) => {
497
- try {
498
- if (options.output) {
499
- validateOutputPath(options.output);
500
- }
501
- const absPath = resolve(process.cwd(), mediaPath);
502
- if (!existsSync(absPath)) {
503
- exitWithError(notFoundError(absPath));
504
- }
505
- if (options.dryRun) {
506
- outputResult({
507
- dryRun: true,
508
- command: "ai highlights",
509
- params: {
510
- mediaPath,
511
- output: options.output,
512
- project: options.project,
513
- duration: options.duration,
514
- count: options.count,
515
- threshold: options.threshold,
516
- criteria: options.criteria,
517
- language: options.language,
518
- useGemini: options.useGemini ?? false,
519
- lowRes: options.lowRes ?? false,
520
- },
521
- });
522
- return;
523
- }
524
- const ext = extname(absPath).toLowerCase();
525
- const videoExtensions = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
526
- const isVideo = videoExtensions.includes(ext);
527
- console.log();
528
- console.log(chalk.bold.cyan("🎬 Highlight Extraction Pipeline"));
529
- console.log(chalk.dim("─".repeat(60)));
530
- if (options.useGemini) {
531
- console.log(chalk.dim("Using Gemini Video Understanding (visual + audio analysis)"));
532
- }
533
- else {
534
- console.log(chalk.dim("Using Whisper + Claude (audio-based analysis)"));
535
- }
536
- console.log();
537
- const targetDuration = options.duration ? parseFloat(options.duration) : undefined;
538
- const maxCount = options.count ? parseInt(options.count) : undefined;
539
- let allHighlights = [];
540
- let sourceDuration = 0;
541
- if (options.useGemini && isVideo) {
542
- const geminiApiKey = await getApiKey("GOOGLE_API_KEY", "Google");
543
- if (!geminiApiKey) {
544
- exitWithError(authError("GOOGLE_API_KEY", "Google"));
545
- }
546
- const durationSpinner = ora("📊 Analyzing video metadata...").start();
547
- try {
548
- sourceDuration = await ffprobeDuration(absPath);
549
- durationSpinner.succeed(chalk.green(`Video duration: ${formatTime(sourceDuration)}`));
550
- }
551
- catch {
552
- durationSpinner.fail("Failed to get video duration");
553
- exitWithError(generalError("Failed to get video duration"));
554
- }
555
- const geminiSpinner = ora("🎬 Analyzing video with Gemini (visual + audio)...").start();
556
- const gemini = new GeminiProvider();
557
- await gemini.initialize({ apiKey: geminiApiKey });
558
- const videoBuffer = await readFile(absPath);
559
- const criteriaText = options.criteria === "all"
560
- ? "emotional, informative, and funny moments"
561
- : `${options.criteria} moments`;
562
- const durationText = targetDuration ? `Target a total highlight duration of ${targetDuration} seconds.` : "";
563
- const countText = maxCount ? `Find up to ${maxCount} highlights.` : "";
564
- const geminiPrompt = `Analyze this video and identify the most engaging highlights based on BOTH visual and audio content.
565
-
566
- Focus on finding ${criteriaText}. ${durationText} ${countText}
567
-
568
- For each highlight, provide:
569
- 1. Start timestamp (in seconds, as a number)
570
- 2. End timestamp (in seconds, as a number)
571
- 3. Category: "emotional", "informative", or "funny"
572
- 4. Confidence score (0-1)
573
- 5. Brief reason why this is a highlight
574
- 6. What is said/shown during this moment
575
-
576
- IMPORTANT: Respond ONLY with valid JSON in this exact format:
577
- {
578
- "highlights": [
579
- {
580
- "startTime": 12.5,
581
- "endTime": 28.3,
582
- "category": "emotional",
583
- "confidence": 0.95,
584
- "reason": "Powerful personal story about overcoming challenges",
585
- "transcript": "When I first started, everyone said it was impossible..."
586
- }
587
- ]
588
- }
589
-
590
- Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID (speech, reactions) to find the most compelling moments.`;
591
- const result = await gemini.analyzeVideo(videoBuffer, geminiPrompt, {
592
- fps: 1,
593
- lowResolution: options.lowRes,
594
- });
595
- if (!result.success || !result.response) {
596
- geminiSpinner.fail("Gemini analysis failed");
597
- exitWithError(apiError(`Gemini analysis failed: ${result.error}`, true));
598
- }
599
- try {
600
- let jsonStr = result.response;
601
- const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
602
- if (jsonMatch)
603
- jsonStr = jsonMatch[1];
604
- const objectMatch = jsonStr.match(/\{[\s\S]*"highlights"[\s\S]*\}/);
605
- if (objectMatch)
606
- jsonStr = objectMatch[0];
607
- const parsed = JSON.parse(jsonStr);
608
- if (parsed.highlights && Array.isArray(parsed.highlights)) {
609
- allHighlights = parsed.highlights.map((h, i) => ({
610
- index: i + 1,
611
- startTime: h.startTime,
612
- endTime: h.endTime,
613
- duration: h.endTime - h.startTime,
614
- category: h.category || "all",
615
- confidence: h.confidence || 0.8,
616
- reason: h.reason || "Engaging moment",
617
- transcript: h.transcript || "",
618
- }));
619
- }
620
- }
621
- catch {
622
- geminiSpinner.fail("Failed to parse Gemini response");
623
- exitWithError(apiError("Failed to parse Gemini response", true));
624
- }
625
- geminiSpinner.succeed(chalk.green(`Found ${allHighlights.length} highlights via visual+audio analysis`));
626
- }
627
- else {
628
- const openaiApiKey = await getApiKey("OPENAI_API_KEY", "OpenAI");
629
- if (!openaiApiKey) {
630
- exitWithError(authError("OPENAI_API_KEY", "OpenAI"));
631
- }
632
- const claudeApiKey = await getApiKey("ANTHROPIC_API_KEY", "Anthropic");
633
- if (!claudeApiKey) {
634
- exitWithError(authError("ANTHROPIC_API_KEY", "Anthropic"));
635
- }
636
- let audioPath = absPath;
637
- let tempAudioPath = null;
638
- if (isVideo) {
639
- const audioSpinner = ora("🎵 Extracting audio from video...").start();
640
- try {
641
- if (!commandExists("ffmpeg")) {
642
- audioSpinner.fail("FFmpeg not found");
643
- exitWithError(generalError("FFmpeg not found. Please install FFmpeg."));
644
- }
645
- const { stdout: probeOut } = await execSafe("ffprobe", [
646
- "-v", "error", "-select_streams", "a", "-show_entries", "stream=codec_type", "-of", "csv=p=0", absPath,
647
- ]);
648
- const hasAudio = probeOut.trim().length > 0;
649
- if (!hasAudio) {
650
- audioSpinner.fail("Video has no audio track");
651
- exitWithError(generalError("Video has no audio track — cannot use Whisper transcription.", "Use --use-gemini flag for visual-only analysis of videos without audio."));
652
- }
653
- else {
654
- tempAudioPath = `/tmp/vibe_highlight_audio_${Date.now()}.wav`;
655
- await execSafe("ffmpeg", [
656
- "-i", absPath, "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", tempAudioPath, "-y",
657
- ], { maxBuffer: 50 * 1024 * 1024 });
658
- audioPath = tempAudioPath;
659
- }
660
- sourceDuration = await ffprobeDuration(absPath);
661
- if (hasAudio) {
662
- audioSpinner.succeed(chalk.green(`Extracted audio (${formatTime(sourceDuration)} total duration)`));
663
- }
664
- }
665
- catch (error) {
666
- audioSpinner.fail("Failed to extract audio");
667
- exitWithError(generalError(`Failed to extract audio: ${error instanceof Error ? error.message : String(error)}`));
668
- }
669
- }
670
- const transcribeSpinner = ora("📝 Transcribing with Whisper...").start();
671
- const whisper = new WhisperProvider();
672
- await whisper.initialize({ apiKey: openaiApiKey });
673
- const audioBuffer = await readFile(audioPath);
674
- const audioBlob = new Blob([audioBuffer]);
675
- const transcriptResult = await whisper.transcribe(audioBlob, options.language);
676
- if (transcriptResult.status === "failed" || !transcriptResult.segments) {
677
- transcribeSpinner.fail("Transcription failed");
678
- if (tempAudioPath && existsSync(tempAudioPath)) {
679
- const { unlink: unlinkFile } = await import("node:fs/promises");
680
- await unlinkFile(tempAudioPath).catch(() => { });
681
- }
682
- exitWithError(apiError(`Transcription failed: ${transcriptResult.error}`, true));
683
- }
684
- transcribeSpinner.succeed(chalk.green(`Transcribed ${transcriptResult.segments.length} segments`));
685
- if (tempAudioPath && existsSync(tempAudioPath)) {
686
- const { unlink: unlinkFile } = await import("node:fs/promises");
687
- await unlinkFile(tempAudioPath).catch(() => { });
688
- }
689
- if (transcriptResult.segments.length > 0) {
690
- sourceDuration = transcriptResult.segments[transcriptResult.segments.length - 1].endTime;
691
- }
692
- const analyzeSpinner = ora("🔍 Analyzing highlights with Claude...").start();
693
- const claude = new ClaudeProvider();
694
- await claude.initialize({ apiKey: claudeApiKey });
695
- allHighlights = await claude.analyzeForHighlights(transcriptResult.segments, {
696
- criteria: options.criteria,
697
- targetDuration,
698
- maxCount,
699
- });
700
- if (allHighlights.length === 0) {
701
- analyzeSpinner.warn(chalk.yellow("No highlights detected in the content"));
702
- process.exit(0);
703
- }
704
- analyzeSpinner.succeed(chalk.green(`Found ${allHighlights.length} potential highlights`));
705
- }
706
- if (allHighlights.length === 0) {
707
- console.log(chalk.yellow("No highlights detected in the content"));
708
- process.exit(0);
709
- }
710
- const filterSpinner = ora("📊 Filtering and ranking...").start();
711
- const threshold = parseFloat(options.threshold);
712
- const filteredHighlights = filterHighlights(allHighlights, { threshold, targetDuration, maxCount });
713
- const totalHighlightDuration = filteredHighlights.reduce((sum, h) => sum + h.duration, 0);
714
- filterSpinner.succeed(chalk.green(`Selected ${filteredHighlights.length} highlights (${totalHighlightDuration.toFixed(1)}s total)`));
715
- const result = {
716
- sourceFile: absPath,
717
- totalDuration: sourceDuration,
718
- criteria: options.criteria,
719
- threshold,
720
- highlightsCount: filteredHighlights.length,
721
- totalHighlightDuration,
722
- highlights: filteredHighlights,
723
- };
724
- console.log();
725
- console.log(chalk.bold.cyan("Highlights Summary"));
726
- console.log(chalk.dim("─".repeat(60)));
727
- for (const highlight of filteredHighlights) {
728
- const startFormatted = formatTime(highlight.startTime);
729
- const endFormatted = formatTime(highlight.endTime);
730
- const confidencePercent = (highlight.confidence * 100).toFixed(0);
731
- const categoryColor = getCategoryColor(highlight.category);
732
- console.log();
733
- console.log(` ${chalk.yellow(`${highlight.index}.`)} [${startFormatted} - ${endFormatted}] ${categoryColor(highlight.category)}, ${chalk.dim(`${confidencePercent}%`)}`);
734
- console.log(` ${chalk.white(highlight.reason)}`);
735
- console.log(` ${chalk.dim(truncate(highlight.transcript, 80))}`);
736
- }
737
- console.log();
738
- console.log(chalk.dim("─".repeat(60)));
739
- console.log(`Total: ${chalk.bold(filteredHighlights.length)} highlights, ${chalk.bold(totalHighlightDuration.toFixed(1))} seconds`);
740
- console.log();
741
- if (options.output) {
742
- const outputPath = resolve(process.cwd(), options.output);
743
- await writeFile(outputPath, JSON.stringify(result, null, 2), "utf-8");
744
- console.log(chalk.green(`💾 Saved highlights to: ${outputPath}`));
745
- }
746
- if (options.project) {
747
- const projectSpinner = ora("📦 Creating project...").start();
748
- const project = new Project("Highlight Reel");
749
- const source = project.addSource({
750
- name: basename(absPath),
751
- url: absPath,
752
- type: isVideo ? "video" : "audio",
753
- duration: sourceDuration,
754
- });
755
- const videoTrack = project.getTracks().find((t) => t.type === "video");
756
- if (!videoTrack) {
757
- projectSpinner.fail("Failed to create project");
758
- exitWithError(generalError("Failed to create project: no video track"));
759
- }
760
- let currentTime = 0;
761
- for (const highlight of filteredHighlights) {
762
- project.addClip({
763
- sourceId: source.id,
764
- trackId: videoTrack.id,
765
- startTime: currentTime,
766
- duration: highlight.duration,
767
- sourceStartOffset: highlight.startTime,
768
- sourceEndOffset: highlight.endTime,
769
- });
770
- currentTime += highlight.duration;
771
- }
772
- const projectPath = resolve(process.cwd(), options.project);
773
- await writeFile(projectPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
774
- projectSpinner.succeed(chalk.green(`Created project: ${projectPath}`));
775
- }
776
- console.log();
777
- console.log(chalk.bold.green("✅ Highlight extraction complete!"));
778
- console.log();
779
- }
780
- catch (error) {
781
- exitWithError(apiError(`Highlight extraction failed: ${error instanceof Error ? error.message : String(error)}`, true));
782
- }
783
- });
784
- aiCommand
785
- .command("auto-shorts")
786
- .alias("shorts")
787
- .description("Auto-generate shorts from long-form video")
788
- .argument("<video>", "Video file path")
789
- .option("-o, --output <path>", "Output file (single) or directory (multiple)")
790
- .option("-d, --duration <seconds>", "Target duration in seconds (15-60)", "60")
791
- .option("-n, --count <number>", "Number of shorts to generate", "1")
792
- .option("-a, --aspect <ratio>", "Aspect ratio: 9:16, 1:1", "9:16")
793
- .option("--output-dir <dir>", "Output directory for multiple shorts")
794
- .option("--add-captions", "Add auto-generated captions")
795
- .option("--caption-style <style>", "Caption style: minimal, bold, animated", "bold")
796
- .option("--analyze-only", "Show segments without generating")
797
- .option("-l, --language <lang>", "Language code for transcription")
798
- .option("--use-gemini", "Use Gemini Video Understanding for enhanced visual+audio analysis")
799
- .option("--low-res", "Use low resolution mode for longer videos (Gemini only)")
800
- .option("--dry-run", "Preview parameters without executing")
801
- .action(async (videoPath, options) => {
802
- try {
803
- if (options.output) {
804
- validateOutputPath(options.output);
805
- }
806
- const absPath = resolve(process.cwd(), videoPath);
807
- if (!existsSync(absPath)) {
808
- exitWithError(notFoundError(absPath));
809
- }
810
- if (options.dryRun) {
811
- outputResult({
812
- dryRun: true,
813
- command: "ai auto-shorts",
814
- params: {
815
- videoPath,
816
- output: options.output,
817
- duration: options.duration,
818
- count: options.count,
819
- aspect: options.aspect,
820
- outputDir: options.outputDir,
821
- addCaptions: options.addCaptions ?? false,
822
- captionStyle: options.captionStyle,
823
- analyzeOnly: options.analyzeOnly ?? false,
824
- language: options.language,
825
- useGemini: options.useGemini ?? false,
826
- lowRes: options.lowRes ?? false,
827
- },
828
- });
829
- return;
830
- }
831
- if (!commandExists("ffmpeg")) {
832
- exitWithError(generalError("FFmpeg not found. Please install FFmpeg."));
833
- }
834
- const targetDuration = parseInt(options.duration);
835
- const shortCount = parseInt(options.count);
836
- console.log();
837
- console.log(chalk.bold.cyan("🎬 Auto Shorts Generator"));
838
- console.log(chalk.dim("─".repeat(60)));
839
- if (options.useGemini) {
840
- console.log(chalk.dim("Using Gemini Video Understanding (visual + audio analysis)"));
841
- }
842
- else {
843
- console.log(chalk.dim("Using Whisper + Claude (audio-based analysis)"));
844
- }
845
- console.log();
846
- let highlights = [];
847
- if (options.useGemini) {
848
- const geminiApiKey = await getApiKey("GOOGLE_API_KEY", "Google");
849
- if (!geminiApiKey) {
850
- exitWithError(authError("GOOGLE_API_KEY", "Google"));
851
- }
852
- const spinner = ora("🎬 Analyzing video with Gemini (visual + audio)...").start();
853
- const gemini = new GeminiProvider();
854
- await gemini.initialize({ apiKey: geminiApiKey });
855
- const videoBuffer = await readFile(absPath);
856
- const geminiPrompt = `Analyze this video to find the BEST moments for short-form vertical video content (TikTok, YouTube Shorts, Instagram Reels).
857
-
858
- Find ${shortCount * 3} potential clips that are ${targetDuration} seconds or shorter each.
859
-
860
- Look for:
861
- - Visually striking or surprising moments
862
- - Emotional peaks (laughter, reactions, reveals)
863
- - Key quotes or memorable statements
864
- - Action sequences or dramatic moments
865
- - Meme-worthy or shareable moments
866
- - Strong hooks (great opening lines)
867
- - Satisfying conclusions
868
-
869
- For each highlight, provide:
870
- 1. Start timestamp (seconds, as number)
871
- 2. End timestamp (seconds, as number) - ensure duration is close to ${targetDuration}s
872
- 3. Virality score (0-1) - how likely this would perform on social media
873
- 4. Hook quality (0-1) - how strong is the opening
874
- 5. Brief reason why this would work as a short
875
-
876
- IMPORTANT: Respond ONLY with valid JSON:
877
- {
878
- "highlights": [
879
- {
880
- "startTime": 45.2,
881
- "endTime": 75.8,
882
- "confidence": 0.92,
883
- "hookQuality": 0.85,
884
- "reason": "Unexpected plot twist with strong visual reaction"
885
- }
886
- ]
887
- }
888
-
889
- Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, reactions, music) to find viral-worthy moments.`;
890
- const result = await gemini.analyzeVideo(videoBuffer, geminiPrompt, {
891
- fps: 1,
892
- lowResolution: options.lowRes,
893
- });
894
- if (!result.success || !result.response) {
895
- spinner.fail("Gemini analysis failed");
896
- exitWithError(apiError(`Gemini analysis failed: ${result.error}`, true));
897
- }
898
- try {
899
- let jsonStr = result.response;
900
- const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
901
- if (jsonMatch)
902
- jsonStr = jsonMatch[1];
903
- const objectMatch = jsonStr.match(/\{[\s\S]*"highlights"[\s\S]*\}/);
904
- if (objectMatch)
905
- jsonStr = objectMatch[0];
906
- const parsed = JSON.parse(jsonStr);
907
- if (parsed.highlights && Array.isArray(parsed.highlights)) {
908
- highlights = parsed.highlights.map((h, i) => ({
909
- index: i + 1,
910
- startTime: h.startTime,
911
- endTime: h.endTime,
912
- duration: h.endTime - h.startTime,
913
- category: "viral",
914
- confidence: h.confidence || 0.8,
915
- reason: h.reason || "Engaging moment",
916
- transcript: "",
917
- }));
918
- }
919
- }
920
- catch {
921
- spinner.fail("Failed to parse Gemini response");
922
- exitWithError(apiError("Failed to parse Gemini response", true));
923
- }
924
- spinner.succeed(chalk.green(`Found ${highlights.length} potential shorts via visual+audio analysis`));
925
- }
926
- else {
927
- const openaiApiKey = await getApiKey("OPENAI_API_KEY", "OpenAI");
928
- if (!openaiApiKey) {
929
- exitWithError(authError("OPENAI_API_KEY", "OpenAI"));
930
- }
931
- const claudeApiKey = await getApiKey("ANTHROPIC_API_KEY", "Anthropic");
932
- if (!claudeApiKey) {
933
- exitWithError(authError("ANTHROPIC_API_KEY", "Anthropic"));
934
- }
935
- const spinner = ora("Extracting audio...").start();
936
- const { stdout: autoShortsProbe } = await execSafe("ffprobe", [
937
- "-v", "error", "-select_streams", "a", "-show_entries", "stream=codec_type", "-of", "csv=p=0", absPath,
938
- ]);
939
- if (!autoShortsProbe.trim()) {
940
- spinner.fail("Video has no audio track");
941
- exitWithError(generalError("Video has no audio track — cannot use Whisper transcription.", "Use --use-gemini flag for visual-only analysis of videos without audio."));
942
- }
943
- const tempAudio = absPath.replace(/(\.[^.]+)$/, "-temp-audio.mp3");
944
- await execSafe("ffmpeg", ["-i", absPath, "-vn", "-acodec", "libmp3lame", "-q:a", "2", tempAudio, "-y"]);
945
- spinner.text = "Transcribing audio...";
946
- const whisper = new WhisperProvider();
947
- await whisper.initialize({ apiKey: openaiApiKey });
948
- const audioBuffer = await readFile(tempAudio);
949
- const audioBlob = new Blob([audioBuffer]);
950
- const transcript = await whisper.transcribe(audioBlob, options.language);
951
- try {
952
- const { unlink: unlinkFile } = await import("node:fs/promises");
953
- await unlinkFile(tempAudio);
954
- }
955
- catch { /* ignore cleanup errors */ }
956
- if (!transcript.segments || transcript.segments.length === 0) {
957
- spinner.fail("No transcript found");
958
- exitWithError(apiError("No transcript found", true));
959
- }
960
- spinner.text = "Finding highlights...";
961
- const claude = new ClaudeProvider();
962
- await claude.initialize({ apiKey: claudeApiKey });
963
- highlights = await claude.analyzeForHighlights(transcript.segments, {
964
- criteria: "all",
965
- targetDuration: targetDuration * shortCount,
966
- maxCount: shortCount * 3,
967
- });
968
- spinner.succeed(chalk.green(`Found ${highlights.length} potential highlights`));
969
- }
970
- if (highlights.length === 0) {
971
- exitWithError(apiError("No highlights found", false));
972
- }
973
- highlights.sort((a, b) => b.confidence - a.confidence);
974
- const selectedHighlights = highlights.slice(0, shortCount);
975
- console.log(chalk.green(`Selected top ${selectedHighlights.length} for short generation`));
976
- console.log();
977
- console.log(chalk.bold.cyan("Auto Shorts"));
978
- console.log(chalk.dim("─".repeat(60)));
979
- console.log(`Target duration: ${targetDuration}s`);
980
- console.log(`Aspect ratio: ${options.aspect}`);
981
- console.log();
982
- for (let i = 0; i < selectedHighlights.length; i++) {
983
- const h = selectedHighlights[i];
984
- console.log(chalk.yellow(`[Short ${i + 1}] ${formatTime(h.startTime)} - ${formatTime(h.endTime)} (${h.duration.toFixed(1)}s)`));
985
- console.log(` ${h.reason}`);
986
- console.log(chalk.dim(` Confidence: ${(h.confidence * 100).toFixed(0)}%`));
987
- }
988
- console.log();
989
- if (options.analyzeOnly) {
990
- console.log(chalk.dim("Use without --analyze-only to generate shorts."));
991
- return;
992
- }
993
- const outputDir = options.outputDir
994
- ? resolve(process.cwd(), options.outputDir)
995
- : dirname(absPath);
996
- if (options.outputDir && !existsSync(outputDir)) {
997
- await mkdir(outputDir, { recursive: true });
998
- }
999
- for (let i = 0; i < selectedHighlights.length; i++) {
1000
- const h = selectedHighlights[i];
1001
- const shortSpinner = ora(`Generating short ${i + 1}/${selectedHighlights.length}...`).start();
1002
- let outputPath;
1003
- if (shortCount === 1 && options.output) {
1004
- outputPath = resolve(process.cwd(), options.output);
1005
- if (!extname(outputPath)) {
1006
- outputPath += ".mp4";
1007
- }
1008
- }
1009
- else {
1010
- const baseName = basename(absPath, extname(absPath));
1011
- outputPath = resolve(outputDir, `${baseName}-short-${i + 1}.mp4`);
1012
- }
1013
- const parentDir = dirname(outputPath);
1014
- if (!existsSync(parentDir)) {
1015
- await mkdir(parentDir, { recursive: true });
1016
- }
1017
- const { stdout: probeOut } = await execSafe("ffprobe", [
1018
- "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=p=0", absPath,
1019
- ]);
1020
- const [width, height] = probeOut.trim().split(",").map(Number);
1021
- const [targetW, targetH] = options.aspect.split(":").map(Number);
1022
- const targetRatio = targetW / targetH;
1023
- const sourceRatio = width / height;
1024
- let cropW, cropH, cropX, cropY;
1025
- if (sourceRatio > targetRatio) {
1026
- cropH = height;
1027
- cropW = Math.round(height * targetRatio);
1028
- cropX = Math.round((width - cropW) / 2);
1029
- cropY = 0;
1030
- }
1031
- else {
1032
- cropW = width;
1033
- cropH = Math.round(width / targetRatio);
1034
- cropX = 0;
1035
- cropY = Math.round((height - cropH) / 2);
1036
- }
1037
- const vf = `crop=${cropW}:${cropH}:${cropX}:${cropY}`;
1038
- await execSafe("ffmpeg", [
1039
- "-ss", String(h.startTime), "-i", absPath, "-t", String(h.duration),
1040
- "-vf", vf, "-c:a", "aac", "-b:a", "128k", outputPath, "-y",
1041
- ], { timeout: 300000 });
1042
- shortSpinner.succeed(chalk.green(`Short ${i + 1}: ${outputPath}`));
1043
- }
1044
- console.log();
1045
- console.log(chalk.bold.green(`Generated ${selectedHighlights.length} short(s)`));
1046
- console.log();
1047
- }
1048
- catch (error) {
1049
- exitWithError(apiError(`Auto shorts failed: ${error instanceof Error ? error.message : String(error)}`, true));
1050
- }
1051
- });
1052
- }
1053
- //# sourceMappingURL=ai-highlights.js.map