@vibeframe/cli 0.55.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 (497) 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-audio-mux.d.ts +0 -75
  112. package/dist/commands/_shared/scene-audio-mux.d.ts.map +0 -1
  113. package/dist/commands/_shared/scene-audio-mux.js +0 -159
  114. package/dist/commands/_shared/scene-audio-mux.js.map +0 -1
  115. package/dist/commands/_shared/scene-audio-mux.test.d.ts +0 -7
  116. package/dist/commands/_shared/scene-audio-mux.test.d.ts.map +0 -1
  117. package/dist/commands/_shared/scene-audio-mux.test.js +0 -94
  118. package/dist/commands/_shared/scene-audio-mux.test.js.map +0 -1
  119. package/dist/commands/_shared/scene-audio-scan.d.ts +0 -94
  120. package/dist/commands/_shared/scene-audio-scan.d.ts.map +0 -1
  121. package/dist/commands/_shared/scene-audio-scan.js +0 -144
  122. package/dist/commands/_shared/scene-audio-scan.js.map +0 -1
  123. package/dist/commands/_shared/scene-audio-scan.test.d.ts +0 -6
  124. package/dist/commands/_shared/scene-audio-scan.test.d.ts.map +0 -1
  125. package/dist/commands/_shared/scene-audio-scan.test.js +0 -190
  126. package/dist/commands/_shared/scene-audio-scan.test.js.map +0 -1
  127. package/dist/commands/_shared/scene-html-emit.d.ts +0 -120
  128. package/dist/commands/_shared/scene-html-emit.d.ts.map +0 -1
  129. package/dist/commands/_shared/scene-html-emit.js +0 -422
  130. package/dist/commands/_shared/scene-html-emit.js.map +0 -1
  131. package/dist/commands/_shared/scene-html-emit.test.d.ts +0 -2
  132. package/dist/commands/_shared/scene-html-emit.test.d.ts.map +0 -1
  133. package/dist/commands/_shared/scene-html-emit.test.js +0 -396
  134. package/dist/commands/_shared/scene-html-emit.test.js.map +0 -1
  135. package/dist/commands/_shared/scene-lint.d.ts +0 -110
  136. package/dist/commands/_shared/scene-lint.d.ts.map +0 -1
  137. package/dist/commands/_shared/scene-lint.js +0 -207
  138. package/dist/commands/_shared/scene-lint.js.map +0 -1
  139. package/dist/commands/_shared/scene-lint.test.d.ts +0 -2
  140. package/dist/commands/_shared/scene-lint.test.d.ts.map +0 -1
  141. package/dist/commands/_shared/scene-lint.test.js +0 -208
  142. package/dist/commands/_shared/scene-lint.test.js.map +0 -1
  143. package/dist/commands/_shared/scene-project.d.ts +0 -101
  144. package/dist/commands/_shared/scene-project.d.ts.map +0 -1
  145. package/dist/commands/_shared/scene-project.js +0 -281
  146. package/dist/commands/_shared/scene-project.js.map +0 -1
  147. package/dist/commands/_shared/scene-project.test.d.ts +0 -2
  148. package/dist/commands/_shared/scene-project.test.d.ts.map +0 -1
  149. package/dist/commands/_shared/scene-project.test.js +0 -203
  150. package/dist/commands/_shared/scene-project.test.js.map +0 -1
  151. package/dist/commands/_shared/scene-render.d.ts +0 -85
  152. package/dist/commands/_shared/scene-render.d.ts.map +0 -1
  153. package/dist/commands/_shared/scene-render.js +0 -178
  154. package/dist/commands/_shared/scene-render.js.map +0 -1
  155. package/dist/commands/_shared/scene-render.test.d.ts +0 -2
  156. package/dist/commands/_shared/scene-render.test.d.ts.map +0 -1
  157. package/dist/commands/_shared/scene-render.test.js +0 -145
  158. package/dist/commands/_shared/scene-render.test.js.map +0 -1
  159. package/dist/commands/_shared/segments-to-scenes.d.ts +0 -85
  160. package/dist/commands/_shared/segments-to-scenes.d.ts.map +0 -1
  161. package/dist/commands/_shared/segments-to-scenes.js +0 -202
  162. package/dist/commands/_shared/segments-to-scenes.js.map +0 -1
  163. package/dist/commands/_shared/segments-to-scenes.test.d.ts +0 -2
  164. package/dist/commands/_shared/segments-to-scenes.test.d.ts.map +0 -1
  165. package/dist/commands/_shared/segments-to-scenes.test.js +0 -219
  166. package/dist/commands/_shared/segments-to-scenes.test.js.map +0 -1
  167. package/dist/commands/_shared/tts-resolve.d.ts +0 -75
  168. package/dist/commands/_shared/tts-resolve.d.ts.map +0 -1
  169. package/dist/commands/_shared/tts-resolve.js +0 -89
  170. package/dist/commands/_shared/tts-resolve.js.map +0 -1
  171. package/dist/commands/_shared/tts-resolve.test.d.ts +0 -6
  172. package/dist/commands/_shared/tts-resolve.test.d.ts.map +0 -1
  173. package/dist/commands/_shared/tts-resolve.test.js +0 -147
  174. package/dist/commands/_shared/tts-resolve.test.js.map +0 -1
  175. package/dist/commands/agent.d.ts +0 -21
  176. package/dist/commands/agent.d.ts.map +0 -1
  177. package/dist/commands/agent.js +0 -285
  178. package/dist/commands/agent.js.map +0 -1
  179. package/dist/commands/ai-analyze.d.ts +0 -106
  180. package/dist/commands/ai-analyze.d.ts.map +0 -1
  181. package/dist/commands/ai-analyze.js +0 -320
  182. package/dist/commands/ai-analyze.js.map +0 -1
  183. package/dist/commands/ai-animated-caption.d.ts +0 -64
  184. package/dist/commands/ai-animated-caption.d.ts.map +0 -1
  185. package/dist/commands/ai-animated-caption.js +0 -272
  186. package/dist/commands/ai-animated-caption.js.map +0 -1
  187. package/dist/commands/ai-audio.d.ts +0 -98
  188. package/dist/commands/ai-audio.d.ts.map +0 -1
  189. package/dist/commands/ai-audio.js +0 -1059
  190. package/dist/commands/ai-audio.js.map +0 -1
  191. package/dist/commands/ai-broll.d.ts +0 -15
  192. package/dist/commands/ai-broll.d.ts.map +0 -1
  193. package/dist/commands/ai-broll.js +0 -421
  194. package/dist/commands/ai-broll.js.map +0 -1
  195. package/dist/commands/ai-edit-cli.d.ts +0 -14
  196. package/dist/commands/ai-edit-cli.d.ts.map +0 -1
  197. package/dist/commands/ai-edit-cli.js +0 -585
  198. package/dist/commands/ai-edit-cli.js.map +0 -1
  199. package/dist/commands/ai-edit.d.ts +0 -398
  200. package/dist/commands/ai-edit.d.ts.map +0 -1
  201. package/dist/commands/ai-edit.js +0 -1060
  202. package/dist/commands/ai-edit.js.map +0 -1
  203. package/dist/commands/ai-fill-gaps.d.ts +0 -14
  204. package/dist/commands/ai-fill-gaps.d.ts.map +0 -1
  205. package/dist/commands/ai-fill-gaps.js +0 -447
  206. package/dist/commands/ai-fill-gaps.js.map +0 -1
  207. package/dist/commands/ai-helpers.d.ts +0 -20
  208. package/dist/commands/ai-helpers.d.ts.map +0 -1
  209. package/dist/commands/ai-helpers.js +0 -59
  210. package/dist/commands/ai-helpers.js.map +0 -1
  211. package/dist/commands/ai-highlights.d.ts +0 -127
  212. package/dist/commands/ai-highlights.d.ts.map +0 -1
  213. package/dist/commands/ai-highlights.js +0 -1053
  214. package/dist/commands/ai-highlights.js.map +0 -1
  215. package/dist/commands/ai-image.d.ts +0 -76
  216. package/dist/commands/ai-image.d.ts.map +0 -1
  217. package/dist/commands/ai-image.js +0 -858
  218. package/dist/commands/ai-image.js.map +0 -1
  219. package/dist/commands/ai-motion.d.ts +0 -50
  220. package/dist/commands/ai-motion.d.ts.map +0 -1
  221. package/dist/commands/ai-motion.js +0 -297
  222. package/dist/commands/ai-motion.js.map +0 -1
  223. package/dist/commands/ai-narrate.d.ts +0 -66
  224. package/dist/commands/ai-narrate.d.ts.map +0 -1
  225. package/dist/commands/ai-narrate.js +0 -332
  226. package/dist/commands/ai-narrate.js.map +0 -1
  227. package/dist/commands/ai-review.d.ts +0 -57
  228. package/dist/commands/ai-review.d.ts.map +0 -1
  229. package/dist/commands/ai-review.js +0 -270
  230. package/dist/commands/ai-review.js.map +0 -1
  231. package/dist/commands/ai-script-pipeline-cli.d.ts +0 -9
  232. package/dist/commands/ai-script-pipeline-cli.d.ts.map +0 -1
  233. package/dist/commands/ai-script-pipeline-cli.js +0 -502
  234. package/dist/commands/ai-script-pipeline-cli.js.map +0 -1
  235. package/dist/commands/ai-script-pipeline.d.ts +0 -305
  236. package/dist/commands/ai-script-pipeline.d.ts.map +0 -1
  237. package/dist/commands/ai-script-pipeline.js +0 -1546
  238. package/dist/commands/ai-script-pipeline.js.map +0 -1
  239. package/dist/commands/ai-suggest-edit.d.ts +0 -14
  240. package/dist/commands/ai-suggest-edit.d.ts.map +0 -1
  241. package/dist/commands/ai-suggest-edit.js +0 -215
  242. package/dist/commands/ai-suggest-edit.js.map +0 -1
  243. package/dist/commands/ai-video-fx.d.ts +0 -14
  244. package/dist/commands/ai-video-fx.d.ts.map +0 -1
  245. package/dist/commands/ai-video-fx.js +0 -396
  246. package/dist/commands/ai-video-fx.js.map +0 -1
  247. package/dist/commands/ai-video.d.ts +0 -91
  248. package/dist/commands/ai-video.d.ts.map +0 -1
  249. package/dist/commands/ai-video.js +0 -1055
  250. package/dist/commands/ai-video.js.map +0 -1
  251. package/dist/commands/ai-viral.d.ts +0 -15
  252. package/dist/commands/ai-viral.d.ts.map +0 -1
  253. package/dist/commands/ai-viral.js +0 -507
  254. package/dist/commands/ai-viral.js.map +0 -1
  255. package/dist/commands/ai-visual-fx.d.ts +0 -14
  256. package/dist/commands/ai-visual-fx.d.ts.map +0 -1
  257. package/dist/commands/ai-visual-fx.js +0 -521
  258. package/dist/commands/ai-visual-fx.js.map +0 -1
  259. package/dist/commands/ai.d.ts +0 -38
  260. package/dist/commands/ai.d.ts.map +0 -1
  261. package/dist/commands/ai.js +0 -225
  262. package/dist/commands/ai.js.map +0 -1
  263. package/dist/commands/ai.test.d.ts +0 -2
  264. package/dist/commands/ai.test.d.ts.map +0 -1
  265. package/dist/commands/ai.test.js +0 -557
  266. package/dist/commands/ai.test.js.map +0 -1
  267. package/dist/commands/analyze.d.ts +0 -16
  268. package/dist/commands/analyze.d.ts.map +0 -1
  269. package/dist/commands/analyze.js +0 -247
  270. package/dist/commands/analyze.js.map +0 -1
  271. package/dist/commands/audio.d.ts +0 -18
  272. package/dist/commands/audio.d.ts.map +0 -1
  273. package/dist/commands/audio.js +0 -539
  274. package/dist/commands/audio.js.map +0 -1
  275. package/dist/commands/batch.d.ts +0 -3
  276. package/dist/commands/batch.d.ts.map +0 -1
  277. package/dist/commands/batch.js +0 -419
  278. package/dist/commands/batch.js.map +0 -1
  279. package/dist/commands/batch.test.d.ts +0 -2
  280. package/dist/commands/batch.test.d.ts.map +0 -1
  281. package/dist/commands/batch.test.js +0 -203
  282. package/dist/commands/batch.test.js.map +0 -1
  283. package/dist/commands/context.d.ts +0 -7
  284. package/dist/commands/context.d.ts.map +0 -1
  285. package/dist/commands/context.js +0 -66
  286. package/dist/commands/context.js.map +0 -1
  287. package/dist/commands/demo.d.ts +0 -7
  288. package/dist/commands/demo.d.ts.map +0 -1
  289. package/dist/commands/demo.js +0 -192
  290. package/dist/commands/demo.js.map +0 -1
  291. package/dist/commands/detect.d.ts +0 -56
  292. package/dist/commands/detect.d.ts.map +0 -1
  293. package/dist/commands/detect.js +0 -464
  294. package/dist/commands/detect.js.map +0 -1
  295. package/dist/commands/doctor.d.ts +0 -6
  296. package/dist/commands/doctor.d.ts.map +0 -1
  297. package/dist/commands/doctor.js +0 -322
  298. package/dist/commands/doctor.js.map +0 -1
  299. package/dist/commands/edit-cmd.d.ts +0 -112
  300. package/dist/commands/edit-cmd.d.ts.map +0 -1
  301. package/dist/commands/edit-cmd.js +0 -1041
  302. package/dist/commands/edit-cmd.js.map +0 -1
  303. package/dist/commands/export.d.ts +0 -61
  304. package/dist/commands/export.d.ts.map +0 -1
  305. package/dist/commands/export.js +0 -980
  306. package/dist/commands/export.js.map +0 -1
  307. package/dist/commands/export.test.d.ts +0 -2
  308. package/dist/commands/export.test.d.ts.map +0 -1
  309. package/dist/commands/export.test.js +0 -107
  310. package/dist/commands/export.test.js.map +0 -1
  311. package/dist/commands/generate.d.ts +0 -84
  312. package/dist/commands/generate.d.ts.map +0 -1
  313. package/dist/commands/generate.js +0 -2030
  314. package/dist/commands/generate.js.map +0 -1
  315. package/dist/commands/media.d.ts +0 -3
  316. package/dist/commands/media.d.ts.map +0 -1
  317. package/dist/commands/media.js +0 -163
  318. package/dist/commands/media.js.map +0 -1
  319. package/dist/commands/output.d.ts +0 -51
  320. package/dist/commands/output.d.ts.map +0 -1
  321. package/dist/commands/output.js +0 -219
  322. package/dist/commands/output.js.map +0 -1
  323. package/dist/commands/output.test.d.ts +0 -2
  324. package/dist/commands/output.test.d.ts.map +0 -1
  325. package/dist/commands/output.test.js +0 -151
  326. package/dist/commands/output.test.js.map +0 -1
  327. package/dist/commands/pipeline.d.ts +0 -19
  328. package/dist/commands/pipeline.d.ts.map +0 -1
  329. package/dist/commands/pipeline.js +0 -345
  330. package/dist/commands/pipeline.js.map +0 -1
  331. package/dist/commands/project.d.ts +0 -3
  332. package/dist/commands/project.d.ts.map +0 -1
  333. package/dist/commands/project.js +0 -181
  334. package/dist/commands/project.js.map +0 -1
  335. package/dist/commands/project.test.d.ts +0 -2
  336. package/dist/commands/project.test.d.ts.map +0 -1
  337. package/dist/commands/project.test.js +0 -105
  338. package/dist/commands/project.test.js.map +0 -1
  339. package/dist/commands/run.d.ts +0 -15
  340. package/dist/commands/run.d.ts.map +0 -1
  341. package/dist/commands/run.js +0 -204
  342. package/dist/commands/run.js.map +0 -1
  343. package/dist/commands/sanitize.d.ts +0 -21
  344. package/dist/commands/sanitize.d.ts.map +0 -1
  345. package/dist/commands/sanitize.js +0 -56
  346. package/dist/commands/sanitize.js.map +0 -1
  347. package/dist/commands/scene.d.ts +0 -86
  348. package/dist/commands/scene.d.ts.map +0 -1
  349. package/dist/commands/scene.js +0 -728
  350. package/dist/commands/scene.js.map +0 -1
  351. package/dist/commands/scene.test.d.ts +0 -2
  352. package/dist/commands/scene.test.d.ts.map +0 -1
  353. package/dist/commands/scene.test.js +0 -219
  354. package/dist/commands/scene.test.js.map +0 -1
  355. package/dist/commands/schema.d.ts +0 -13
  356. package/dist/commands/schema.d.ts.map +0 -1
  357. package/dist/commands/schema.js +0 -218
  358. package/dist/commands/schema.js.map +0 -1
  359. package/dist/commands/setup.d.ts +0 -6
  360. package/dist/commands/setup.d.ts.map +0 -1
  361. package/dist/commands/setup.js +0 -461
  362. package/dist/commands/setup.js.map +0 -1
  363. package/dist/commands/timeline.d.ts +0 -3
  364. package/dist/commands/timeline.d.ts.map +0 -1
  365. package/dist/commands/timeline.js +0 -609
  366. package/dist/commands/timeline.js.map +0 -1
  367. package/dist/commands/timeline.test.d.ts +0 -2
  368. package/dist/commands/timeline.test.d.ts.map +0 -1
  369. package/dist/commands/timeline.test.js +0 -334
  370. package/dist/commands/timeline.test.js.map +0 -1
  371. package/dist/commands/validate.d.ts +0 -34
  372. package/dist/commands/validate.d.ts.map +0 -1
  373. package/dist/commands/validate.js +0 -70
  374. package/dist/commands/validate.js.map +0 -1
  375. package/dist/config/config.test.d.ts +0 -2
  376. package/dist/config/config.test.d.ts.map +0 -1
  377. package/dist/config/config.test.js +0 -164
  378. package/dist/config/config.test.js.map +0 -1
  379. package/dist/config/index.d.ts +0 -35
  380. package/dist/config/index.d.ts.map +0 -1
  381. package/dist/config/index.js +0 -101
  382. package/dist/config/index.js.map +0 -1
  383. package/dist/config/schema.d.ts +0 -52
  384. package/dist/config/schema.d.ts.map +0 -1
  385. package/dist/config/schema.js +0 -44
  386. package/dist/config/schema.js.map +0 -1
  387. package/dist/engine/index.d.ts +0 -3
  388. package/dist/engine/index.d.ts.map +0 -1
  389. package/dist/engine/index.js +0 -2
  390. package/dist/engine/index.js.map +0 -1
  391. package/dist/engine/project.d.ts +0 -84
  392. package/dist/engine/project.d.ts.map +0 -1
  393. package/dist/engine/project.js +0 -355
  394. package/dist/engine/project.js.map +0 -1
  395. package/dist/engine/project.test.d.ts +0 -2
  396. package/dist/engine/project.test.d.ts.map +0 -1
  397. package/dist/engine/project.test.js +0 -599
  398. package/dist/engine/project.test.js.map +0 -1
  399. package/dist/index.d.ts +0 -7
  400. package/dist/index.d.ts.map +0 -1
  401. package/dist/index.js.map +0 -1
  402. package/dist/pipeline/executor.d.ts +0 -19
  403. package/dist/pipeline/executor.d.ts.map +0 -1
  404. package/dist/pipeline/executor.js +0 -397
  405. package/dist/pipeline/executor.js.map +0 -1
  406. package/dist/pipeline/index.d.ts +0 -4
  407. package/dist/pipeline/index.d.ts.map +0 -1
  408. package/dist/pipeline/index.js +0 -2
  409. package/dist/pipeline/index.js.map +0 -1
  410. package/dist/pipeline/renderers/__tests__/html-template.test.d.ts +0 -2
  411. package/dist/pipeline/renderers/__tests__/html-template.test.d.ts.map +0 -1
  412. package/dist/pipeline/renderers/__tests__/html-template.test.js +0 -170
  413. package/dist/pipeline/renderers/__tests__/html-template.test.js.map +0 -1
  414. package/dist/pipeline/renderers/__tests__/integration.test.d.ts +0 -2
  415. package/dist/pipeline/renderers/__tests__/integration.test.d.ts.map +0 -1
  416. package/dist/pipeline/renderers/__tests__/integration.test.js +0 -175
  417. package/dist/pipeline/renderers/__tests__/integration.test.js.map +0 -1
  418. package/dist/pipeline/renderers/chrome.d.ts +0 -26
  419. package/dist/pipeline/renderers/chrome.d.ts.map +0 -1
  420. package/dist/pipeline/renderers/chrome.js +0 -48
  421. package/dist/pipeline/renderers/chrome.js.map +0 -1
  422. package/dist/pipeline/renderers/html-clips.d.ts +0 -20
  423. package/dist/pipeline/renderers/html-clips.d.ts.map +0 -1
  424. package/dist/pipeline/renderers/html-clips.js +0 -50
  425. package/dist/pipeline/renderers/html-clips.js.map +0 -1
  426. package/dist/pipeline/renderers/html-runtime.d.ts +0 -7
  427. package/dist/pipeline/renderers/html-runtime.d.ts.map +0 -1
  428. package/dist/pipeline/renderers/html-runtime.js +0 -80
  429. package/dist/pipeline/renderers/html-runtime.js.map +0 -1
  430. package/dist/pipeline/renderers/html-template.d.ts +0 -3
  431. package/dist/pipeline/renderers/html-template.d.ts.map +0 -1
  432. package/dist/pipeline/renderers/html-template.js +0 -36
  433. package/dist/pipeline/renderers/html-template.js.map +0 -1
  434. package/dist/pipeline/renderers/hyperframes.d.ts +0 -8
  435. package/dist/pipeline/renderers/hyperframes.d.ts.map +0 -1
  436. package/dist/pipeline/renderers/hyperframes.js +0 -63
  437. package/dist/pipeline/renderers/hyperframes.js.map +0 -1
  438. package/dist/pipeline/renderers/project-builder.d.ts +0 -8
  439. package/dist/pipeline/renderers/project-builder.d.ts.map +0 -1
  440. package/dist/pipeline/renderers/project-builder.js +0 -65
  441. package/dist/pipeline/renderers/project-builder.js.map +0 -1
  442. package/dist/pipeline/renderers/types.d.ts +0 -41
  443. package/dist/pipeline/renderers/types.d.ts.map +0 -1
  444. package/dist/pipeline/renderers/types.js +0 -11
  445. package/dist/pipeline/renderers/types.js.map +0 -1
  446. package/dist/pipeline/resolver.d.ts +0 -20
  447. package/dist/pipeline/resolver.d.ts.map +0 -1
  448. package/dist/pipeline/resolver.js +0 -74
  449. package/dist/pipeline/resolver.js.map +0 -1
  450. package/dist/pipeline/types.d.ts +0 -82
  451. package/dist/pipeline/types.d.ts.map +0 -1
  452. package/dist/pipeline/types.js +0 -8
  453. package/dist/pipeline/types.js.map +0 -1
  454. package/dist/utils/api-key.d.ts +0 -33
  455. package/dist/utils/api-key.d.ts.map +0 -1
  456. package/dist/utils/api-key.js +0 -228
  457. package/dist/utils/api-key.js.map +0 -1
  458. package/dist/utils/api-key.test.d.ts +0 -2
  459. package/dist/utils/api-key.test.d.ts.map +0 -1
  460. package/dist/utils/api-key.test.js +0 -35
  461. package/dist/utils/api-key.test.js.map +0 -1
  462. package/dist/utils/audio.d.ts +0 -23
  463. package/dist/utils/audio.d.ts.map +0 -1
  464. package/dist/utils/audio.js +0 -79
  465. package/dist/utils/audio.js.map +0 -1
  466. package/dist/utils/exec-safe.d.ts +0 -22
  467. package/dist/utils/exec-safe.d.ts.map +0 -1
  468. package/dist/utils/exec-safe.js +0 -62
  469. package/dist/utils/exec-safe.js.map +0 -1
  470. package/dist/utils/first-run.d.ts +0 -17
  471. package/dist/utils/first-run.d.ts.map +0 -1
  472. package/dist/utils/first-run.js +0 -73
  473. package/dist/utils/first-run.js.map +0 -1
  474. package/dist/utils/input-validation.d.ts +0 -27
  475. package/dist/utils/input-validation.d.ts.map +0 -1
  476. package/dist/utils/input-validation.js +0 -67
  477. package/dist/utils/input-validation.js.map +0 -1
  478. package/dist/utils/provider-resolver.d.ts +0 -19
  479. package/dist/utils/provider-resolver.d.ts.map +0 -1
  480. package/dist/utils/provider-resolver.js +0 -75
  481. package/dist/utils/provider-resolver.js.map +0 -1
  482. package/dist/utils/remotion.d.ts +0 -231
  483. package/dist/utils/remotion.d.ts.map +0 -1
  484. package/dist/utils/remotion.js +0 -879
  485. package/dist/utils/remotion.js.map +0 -1
  486. package/dist/utils/subtitle.d.ts +0 -65
  487. package/dist/utils/subtitle.d.ts.map +0 -1
  488. package/dist/utils/subtitle.js +0 -135
  489. package/dist/utils/subtitle.js.map +0 -1
  490. package/dist/utils/subtitle.test.d.ts +0 -2
  491. package/dist/utils/subtitle.test.d.ts.map +0 -1
  492. package/dist/utils/subtitle.test.js +0 -175
  493. package/dist/utils/subtitle.test.js.map +0 -1
  494. package/dist/utils/tty.d.ts +0 -45
  495. package/dist/utils/tty.d.ts.map +0 -1
  496. package/dist/utils/tty.js +0 -231
  497. 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