@vibeframe/cli 0.55.0 → 0.55.2

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 +63450 -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,1060 +0,0 @@
1
- /**
2
- * @module ai-edit
3
- *
4
- * Video/audio editing execute functions and supporting types.
5
- *
6
- * CLI commands: silence-cut, jump-cut, caption, noise-reduce, fade,
7
- * translate-srt, text-overlay
8
- *
9
- * Execute functions (also used by agent tools via ai.ts re-exports):
10
- * executeSilenceCut, executeJumpCut, executeCaption, executeNoiseReduce,
11
- * executeFade, executeTranslateSrt, applyTextOverlays, executeTextOverlay
12
- *
13
- * CLI command registrations live in ai-edit-cli.ts (registerEditCommands).
14
- * Extracted from ai.ts as part of modularisation.
15
- *
16
- * @dependencies FFmpeg, Whisper (OpenAI), Gemini (Google), Claude/OpenAI (translation)
17
- */
18
- import { resolve, dirname, basename, extname, join } from 'node:path';
19
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
20
- import { existsSync } from 'node:fs';
21
- import { GeminiProvider, WhisperProvider, } from '@vibeframe/ai-providers';
22
- import { getApiKey } from '../utils/api-key.js';
23
- import { getVideoDuration } from '../utils/audio.js';
24
- import { formatSRT, parseSRT } from '../utils/subtitle.js';
25
- import { execSafe, commandExists } from '../utils/exec-safe.js';
26
- /**
27
- * Detect silent periods in a media file using FFmpeg silencedetect
28
- */
29
- async function detectSilencePeriods(videoPath, noiseThreshold, minDuration) {
30
- // Get total duration
31
- const totalDuration = await getVideoDuration(videoPath);
32
- // Run silence detection
33
- const { stdout, stderr } = await execSafe("ffmpeg", [
34
- "-i", videoPath,
35
- "-af", `silencedetect=noise=${noiseThreshold}dB:d=${minDuration}`,
36
- "-f", "null", "-",
37
- ], { maxBuffer: 50 * 1024 * 1024 }).catch((err) => {
38
- // ffmpeg writes filter output to stderr and exits non-zero with -f null
39
- if (err.stdout !== undefined || err.stderr !== undefined) {
40
- return { stdout: err.stdout || "", stderr: err.stderr || "" };
41
- }
42
- throw err;
43
- });
44
- const silenceOutput = stdout + stderr;
45
- const periods = [];
46
- const startRegex = /silence_start: (\d+\.?\d*)/g;
47
- const endRegex = /silence_end: (\d+\.?\d*) \| silence_duration: (\d+\.?\d*)/g;
48
- const starts = [];
49
- let match;
50
- while ((match = startRegex.exec(silenceOutput)) !== null) {
51
- starts.push(parseFloat(match[1]));
52
- }
53
- let i = 0;
54
- while ((match = endRegex.exec(silenceOutput)) !== null) {
55
- const end = parseFloat(match[1]);
56
- const duration = parseFloat(match[2]);
57
- const start = i < starts.length ? starts[i] : end - duration;
58
- periods.push({ start, end, duration });
59
- i++;
60
- }
61
- return { periods, totalDuration };
62
- }
63
- /**
64
- * Detect silent/dead segments using Gemini Video Understanding (multimodal analysis)
65
- */
66
- async function detectSilencePeriodsWithGemini(videoPath, minDuration, options) {
67
- const totalDuration = await getVideoDuration(videoPath);
68
- const geminiApiKey = options.apiKey || await getApiKey("GOOGLE_API_KEY", "Google");
69
- if (!geminiApiKey) {
70
- throw new Error("Google API key required for Gemini Video Understanding. Run 'vibe setup' or set GOOGLE_API_KEY in .env");
71
- }
72
- const gemini = new GeminiProvider();
73
- await gemini.initialize({ apiKey: geminiApiKey });
74
- const videoBuffer = await readFile(videoPath);
75
- // Map model shorthand to full model ID
76
- const modelMap = {
77
- flash: "gemini-3-flash-preview",
78
- "flash-2.5": "gemini-2.5-flash",
79
- pro: "gemini-2.5-pro",
80
- };
81
- const modelId = options.model ? (modelMap[options.model] || modelMap.flash) : undefined;
82
- const prompt = `Analyze this video and identify all silent or dead segments where there is NO meaningful content.
83
-
84
- Detect these as silent/dead segments:
85
- - Complete silence (no audio at all)
86
- - Dead air / ambient noise with no speech or meaningful sound
87
- - Long pauses between speakers or topics (${minDuration}+ seconds)
88
- - Technical silence (e.g., blank screen with no audio)
89
- - Sections with only background noise and no intentional content
90
-
91
- Do NOT mark these as silent (keep them):
92
- - Intentional dramatic pauses (short, part of storytelling)
93
- - Music-only sections (background music, intros, outros)
94
- - Natural breathing pauses within sentences (under ${minDuration} seconds)
95
- - Applause, laughter, or audience reactions
96
- - Sound effects or ambient audio that is part of the content
97
-
98
- Only include segments that are at least ${minDuration} seconds long.
99
- The video total duration is ${totalDuration.toFixed(1)} seconds.
100
-
101
- IMPORTANT: Respond ONLY with valid JSON in this exact format:
102
- {
103
- "silentSegments": [
104
- {
105
- "start": 5.2,
106
- "end": 8.7,
107
- "reason": "Dead air between speakers"
108
- }
109
- ]
110
- }
111
-
112
- If there are no silent segments, return: { "silentSegments": [] }`;
113
- const result = await gemini.analyzeVideo(videoBuffer, prompt, {
114
- fps: 1,
115
- lowResolution: options.lowRes,
116
- ...(modelId ? { model: modelId } : {}),
117
- });
118
- if (!result.success || !result.response) {
119
- throw new Error(`Gemini analysis failed: ${result.error || "No response"}`);
120
- }
121
- // Parse JSON from Gemini response
122
- let jsonStr = result.response;
123
- const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
124
- if (jsonMatch)
125
- jsonStr = jsonMatch[1];
126
- const objectMatch = jsonStr.match(/\{[\s\S]*"silentSegments"[\s\S]*\}/);
127
- if (objectMatch)
128
- jsonStr = objectMatch[0];
129
- const parsed = JSON.parse(jsonStr);
130
- const periods = [];
131
- if (parsed.silentSegments && Array.isArray(parsed.silentSegments)) {
132
- for (const seg of parsed.silentSegments) {
133
- const rawStart = Number(seg.start);
134
- const rawEnd = Number(seg.end);
135
- if (isNaN(rawStart) || isNaN(rawEnd))
136
- continue;
137
- // Clamp to video duration, then validate
138
- const start = Math.max(0, rawStart);
139
- const end = Math.min(rawEnd, totalDuration);
140
- const duration = end - start;
141
- if (duration >= minDuration) {
142
- periods.push({ start, end, duration });
143
- }
144
- }
145
- }
146
- // Sort by start time
147
- periods.sort((a, b) => a.start - b.start);
148
- return { periods, totalDuration };
149
- }
150
- /**
151
- * Remove silent segments from a video using FFmpeg or Gemini detection.
152
- *
153
- * Detects silence via FFmpeg silencedetect (default) or Gemini multimodal
154
- * analysis, then trims and concatenates the non-silent segments.
155
- *
156
- * @param options - Silence cut configuration
157
- * @returns Result with output path and detected silent periods
158
- */
159
- export async function executeSilenceCut(options) {
160
- const { videoPath, outputPath, noiseThreshold = -30, minDuration = 0.5, padding = 0.1, analyzeOnly = false, useGemini = false, } = options;
161
- if (!existsSync(videoPath)) {
162
- return { success: false, error: `Video not found: ${videoPath}` };
163
- }
164
- if (!commandExists("ffmpeg")) {
165
- return { success: false, error: "FFmpeg not found. Please install FFmpeg." };
166
- }
167
- const method = useGemini ? "gemini" : "ffmpeg";
168
- try {
169
- const { periods, totalDuration } = useGemini
170
- ? await detectSilencePeriodsWithGemini(videoPath, minDuration, {
171
- model: options.model,
172
- lowRes: options.lowRes,
173
- apiKey: options.apiKey,
174
- })
175
- : await detectSilencePeriods(videoPath, noiseThreshold, minDuration);
176
- const silentDuration = periods.reduce((sum, p) => sum + p.duration, 0);
177
- if (analyzeOnly || periods.length === 0) {
178
- return {
179
- success: true,
180
- totalDuration,
181
- silentPeriods: periods,
182
- silentDuration,
183
- method,
184
- };
185
- }
186
- // Compute non-silent segments with padding
187
- const segments = [];
188
- let cursor = 0;
189
- for (const period of periods) {
190
- const segEnd = Math.min(period.start + padding, totalDuration);
191
- if (segEnd > cursor) {
192
- segments.push({ start: Math.max(0, cursor - padding), end: segEnd });
193
- }
194
- cursor = period.end;
195
- }
196
- // Add final segment after last silence
197
- if (cursor < totalDuration) {
198
- segments.push({ start: Math.max(0, cursor - padding), end: totalDuration });
199
- }
200
- if (segments.length === 0) {
201
- return { success: false, error: "No non-silent segments found" };
202
- }
203
- // Build filter_complex with trim+concat per segment.
204
- // aselect is broken on FFmpeg 8.x (audio duration unchanged), so we use
205
- // atrim/trim per segment and concat them all.
206
- const vParts = [];
207
- const aParts = [];
208
- const concatInputs = [];
209
- for (let i = 0; i < segments.length; i++) {
210
- const s = segments[i].start.toFixed(4);
211
- const e = segments[i].end.toFixed(4);
212
- vParts.push(`[0:v]trim=${s}:${e},setpts=PTS-STARTPTS[v${i}]`);
213
- aParts.push(`[0:a]atrim=${s}:${e},asetpts=PTS-STARTPTS[a${i}]`);
214
- concatInputs.push(`[v${i}][a${i}]`);
215
- }
216
- const filterComplex = [
217
- ...vParts,
218
- ...aParts,
219
- `${concatInputs.join("")}concat=n=${segments.length}:v=1:a=1[outv][outa]`,
220
- ].join(";");
221
- await execSafe("ffmpeg", [
222
- "-i", videoPath,
223
- "-filter_complex", filterComplex,
224
- "-map", "[outv]", "-map", "[outa]",
225
- "-c:v", "libx264", "-preset", "fast", "-crf", "18",
226
- "-c:a", "aac", "-b:a", "192k",
227
- outputPath, "-y",
228
- ], { timeout: 600000, maxBuffer: 50 * 1024 * 1024 });
229
- return {
230
- success: true,
231
- outputPath,
232
- totalDuration,
233
- silentPeriods: periods,
234
- silentDuration,
235
- method,
236
- };
237
- }
238
- catch (error) {
239
- return {
240
- success: false,
241
- error: `Silence cut failed: ${error instanceof Error ? error.message : String(error)}`,
242
- };
243
- }
244
- }
245
- /** Default set of filler words detected by jump-cut. */
246
- export const DEFAULT_FILLER_WORDS = [
247
- "um", "uh", "uh-huh", "hmm", "like", "you know", "so",
248
- "basically", "literally", "right", "okay", "well", "i mean", "actually",
249
- ];
250
- /**
251
- * Transcribe audio with word-level timestamps using Whisper API directly.
252
- * Uses timestamp_granularities[]=word for filler detection.
253
- */
254
- export async function transcribeWithWords(audioPath, apiKey, language) {
255
- const audioBuffer = await readFile(audioPath);
256
- const audioBlob = new Blob([audioBuffer]);
257
- const formData = new FormData();
258
- formData.append("file", audioBlob, "audio.wav");
259
- formData.append("model", "whisper-1");
260
- formData.append("response_format", "verbose_json");
261
- formData.append("timestamp_granularities[]", "word");
262
- if (language) {
263
- formData.append("language", language);
264
- }
265
- const response = await fetch("https://api.openai.com/v1/audio/transcriptions", {
266
- method: "POST",
267
- headers: {
268
- Authorization: `Bearer ${apiKey}`,
269
- },
270
- body: formData,
271
- });
272
- if (!response.ok) {
273
- const error = await response.text();
274
- throw new Error(`Whisper transcription failed: ${error}`);
275
- }
276
- const data = await response.json();
277
- return {
278
- words: data.words || [],
279
- text: data.text,
280
- };
281
- }
282
- /**
283
- * Detect filler word ranges and merge adjacent ones within padding distance.
284
- *
285
- * @param words - Word-level transcript with timestamps
286
- * @param fillers - List of filler words/phrases to match
287
- * @param padding - Maximum gap in seconds to merge adjacent fillers
288
- * @returns Merged filler word ranges sorted by start time
289
- */
290
- export function detectFillerRanges(words, fillers, padding) {
291
- const fillerSet = new Set(fillers.map((f) => f.toLowerCase().trim()));
292
- // Find individual filler words
293
- const matches = [];
294
- for (const w of words) {
295
- const cleaned = w.word.toLowerCase().replace(/[^a-z\s-]/g, "").trim();
296
- if (fillerSet.has(cleaned)) {
297
- matches.push({ word: w.word, start: w.start, end: w.end });
298
- }
299
- }
300
- if (matches.length === 0)
301
- return [];
302
- // Merge adjacent filler ranges (within padding distance)
303
- const merged = [{ ...matches[0] }];
304
- for (let i = 1; i < matches.length; i++) {
305
- const last = merged[merged.length - 1];
306
- if (matches[i].start - last.end <= padding * 2) {
307
- last.end = matches[i].end;
308
- last.word += ` ${matches[i].word}`;
309
- }
310
- else {
311
- merged.push({ ...matches[i] });
312
- }
313
- }
314
- return merged;
315
- }
316
- /**
317
- * Remove filler words from a video using Whisper word-level timestamps + FFmpeg concat.
318
- *
319
- * Pipeline: extract audio -> Whisper transcription (word-level) -> detect fillers ->
320
- * invert to keep-segments -> FFmpeg stream-copy concat.
321
- *
322
- * @param options - Jump cut configuration
323
- * @returns Result with output path and detected fillers
324
- */
325
- export async function executeJumpCut(options) {
326
- const { videoPath, outputPath, fillers = DEFAULT_FILLER_WORDS, padding = 0.05, language, analyzeOnly = false, apiKey, } = options;
327
- if (!existsSync(videoPath)) {
328
- return { success: false, error: `Video not found: ${videoPath}` };
329
- }
330
- if (!commandExists("ffmpeg")) {
331
- return { success: false, error: "FFmpeg not found. Please install FFmpeg." };
332
- }
333
- const openaiKey = apiKey || process.env.OPENAI_API_KEY;
334
- if (!openaiKey) {
335
- return { success: false, error: "OpenAI API key required for Whisper transcription. Run 'vibe setup' or set OPENAI_API_KEY in .env" };
336
- }
337
- try {
338
- const tmpDir = `/tmp/vibe_jumpcut_${Date.now()}`;
339
- await mkdir(tmpDir, { recursive: true });
340
- const audioPath = join(tmpDir, "audio.wav");
341
- try {
342
- // Step 1: Extract audio
343
- await execSafe("ffmpeg", [
344
- "-i", videoPath, "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", audioPath, "-y",
345
- ], { timeout: 300000, maxBuffer: 50 * 1024 * 1024 });
346
- // Step 2: Transcribe with word-level timestamps
347
- const { words } = await transcribeWithWords(audioPath, openaiKey, language);
348
- if (words.length === 0) {
349
- return { success: false, error: "No words detected in audio" };
350
- }
351
- // Step 3: Detect filler ranges
352
- const fillerRanges = detectFillerRanges(words, fillers, padding);
353
- const totalDuration = await getVideoDuration(videoPath);
354
- const fillerDuration = fillerRanges.reduce((sum, f) => sum + (f.end - f.start), 0);
355
- if (analyzeOnly || fillerRanges.length === 0) {
356
- return {
357
- success: true,
358
- totalDuration,
359
- fillerCount: fillerRanges.length,
360
- fillerDuration,
361
- fillers: fillerRanges,
362
- };
363
- }
364
- // Step 4: Compute keep-segments (invert filler ranges)
365
- const segments = [];
366
- let cursor = 0;
367
- for (const filler of fillerRanges) {
368
- const segStart = Math.max(0, cursor);
369
- const segEnd = Math.max(segStart, filler.start - padding);
370
- if (segEnd > segStart) {
371
- segments.push({ start: segStart, end: segEnd });
372
- }
373
- cursor = filler.end + padding;
374
- }
375
- // Add final segment after last filler
376
- if (cursor < totalDuration) {
377
- segments.push({ start: cursor, end: totalDuration });
378
- }
379
- if (segments.length === 0) {
380
- return { success: false, error: "No non-filler segments found" };
381
- }
382
- // Step 5: Extract segments and concat with FFmpeg (stream copy)
383
- const segmentPaths = [];
384
- for (let i = 0; i < segments.length; i++) {
385
- const seg = segments[i];
386
- const segPath = join(tmpDir, `seg-${i.toString().padStart(4, "0")}.ts`);
387
- const duration = seg.end - seg.start;
388
- await execSafe("ffmpeg", [
389
- "-i", videoPath, "-ss", String(seg.start), "-t", String(duration),
390
- "-c", "copy", "-avoid_negative_ts", "make_zero", segPath, "-y",
391
- ], { timeout: 300000, maxBuffer: 50 * 1024 * 1024 });
392
- segmentPaths.push(segPath);
393
- }
394
- // Create concat list
395
- const concatList = segmentPaths.map((p) => `file '${p}'`).join("\n");
396
- const listPath = join(tmpDir, "concat.txt");
397
- await writeFile(listPath, concatList);
398
- // Concat segments
399
- await execSafe("ffmpeg", [
400
- "-f", "concat", "-safe", "0", "-i", listPath, "-c", "copy", outputPath, "-y",
401
- ], { timeout: 300000, maxBuffer: 50 * 1024 * 1024 });
402
- return {
403
- success: true,
404
- outputPath,
405
- totalDuration,
406
- fillerCount: fillerRanges.length,
407
- fillerDuration,
408
- fillers: fillerRanges,
409
- };
410
- }
411
- finally {
412
- // Cleanup temp files
413
- try {
414
- const { rm } = await import("node:fs/promises");
415
- await rm(tmpDir, { recursive: true, force: true });
416
- }
417
- catch {
418
- // Ignore cleanup errors
419
- }
420
- }
421
- }
422
- catch (error) {
423
- return {
424
- success: false,
425
- error: `Jump cut failed: ${error instanceof Error ? error.message : String(error)}`,
426
- };
427
- }
428
- }
429
- /**
430
- * Get ASS force_style string for caption preset
431
- */
432
- function getCaptionForceStyle(style, fontSize, fontColor, position) {
433
- // ASS alignment: 1-3 bottom, 4-6 middle, 7-9 top (left/center/right)
434
- const alignment = position === "top" ? 8 : position === "center" ? 5 : 2;
435
- const marginV = position === "center" ? 0 : 30;
436
- switch (style) {
437
- case "minimal":
438
- return `FontSize=${fontSize},FontName=Arial,PrimaryColour=&H00FFFFFF,OutlineColour=&H80000000,Outline=1,Shadow=0,Alignment=${alignment},MarginV=${marginV}`;
439
- case "bold":
440
- return `FontSize=${fontSize},FontName=Arial,Bold=1,PrimaryColour=&H00${fontColor === "yellow" ? "00FFFF" : "FFFFFF"},OutlineColour=&H00000000,Outline=3,Shadow=1,Alignment=${alignment},MarginV=${marginV}`;
441
- case "outline":
442
- return `FontSize=${fontSize},FontName=Arial,Bold=1,PrimaryColour=&H00FFFFFF,OutlineColour=&H000000FF,Outline=4,Shadow=0,Alignment=${alignment},MarginV=${marginV}`;
443
- case "karaoke":
444
- return `FontSize=${fontSize},FontName=Arial,Bold=1,PrimaryColour=&H0000FFFF,OutlineColour=&H00000000,Outline=2,Shadow=1,Alignment=${alignment},MarginV=${marginV}`;
445
- default:
446
- return `FontSize=${fontSize},FontName=Arial,Bold=1,PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,Outline=3,Shadow=1,Alignment=${alignment},MarginV=${marginV}`;
447
- }
448
- }
449
- /**
450
- * Transcribe video audio and burn styled captions using Whisper + FFmpeg.
451
- *
452
- * Pipeline: extract audio -> Whisper transcription -> generate SRT ->
453
- * burn captions via FFmpeg subtitles filter (or Remotion fallback).
454
- *
455
- * @param options - Caption configuration
456
- * @returns Result with output video path and SRT path
457
- */
458
- export async function executeCaption(options) {
459
- const { videoPath, outputPath, style = "bold", fontSize: customFontSize, fontColor = "white", language, position = "bottom", apiKey, } = options;
460
- if (!existsSync(videoPath)) {
461
- return { success: false, error: `Video not found: ${videoPath}` };
462
- }
463
- if (!commandExists("ffmpeg")) {
464
- return { success: false, error: "FFmpeg not found. Please install FFmpeg." };
465
- }
466
- const openaiKey = apiKey || process.env.OPENAI_API_KEY;
467
- if (!openaiKey) {
468
- return { success: false, error: "OpenAI API key required for Whisper transcription. Run 'vibe setup' or set OPENAI_API_KEY in .env" };
469
- }
470
- try {
471
- // Step 1: Extract audio from video
472
- const tmpDir = `/tmp/vibe_caption_${Date.now()}`;
473
- await mkdir(tmpDir, { recursive: true });
474
- const audioPath = join(tmpDir, "audio.wav");
475
- const srtPath = join(tmpDir, "captions.srt");
476
- try {
477
- await execSafe("ffmpeg", [
478
- "-i", videoPath, "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", audioPath, "-y",
479
- ], { timeout: 300000, maxBuffer: 50 * 1024 * 1024 });
480
- // Step 2: Transcribe with Whisper
481
- const whisper = new WhisperProvider();
482
- await whisper.initialize({ apiKey: openaiKey });
483
- const audioBuffer = await readFile(audioPath);
484
- const audioBlob = new Blob([audioBuffer]);
485
- const transcriptResult = await whisper.transcribe(audioBlob, language);
486
- if (transcriptResult.status === "failed" || !transcriptResult.segments || transcriptResult.segments.length === 0) {
487
- return { success: false, error: `Transcription failed: ${transcriptResult.error || "No segments detected"}` };
488
- }
489
- // Step 3: Generate SRT
490
- const srtContent = formatSRT(transcriptResult.segments);
491
- await writeFile(srtPath, srtContent);
492
- // Step 4: Get video resolution for auto font size
493
- const { width, height } = await getVideoResolution(videoPath);
494
- const fontSize = customFontSize || Math.round(height / 18);
495
- // Step 5: Check FFmpeg subtitle filter support
496
- let hasSubtitles = false;
497
- try {
498
- const { stdout: filterList } = await execSafe("ffmpeg", ["-filters"], { maxBuffer: 10 * 1024 * 1024 });
499
- hasSubtitles = filterList.includes("subtitles");
500
- }
501
- catch {
502
- // If filter check fails, continue and let FFmpeg error naturally
503
- }
504
- // Step 6: Burn captions
505
- if (hasSubtitles) {
506
- // Fast path: FFmpeg subtitles filter (requires libass)
507
- const forceStyle = getCaptionForceStyle(style, fontSize, fontColor, position);
508
- const escapedSrtPath = srtPath.replace(/\\/g, "/").replace(/:/g, "\\:");
509
- await execSafe("ffmpeg", [
510
- "-i", videoPath, "-vf", `subtitles=${escapedSrtPath}:force_style='${forceStyle}'`,
511
- "-c:a", "copy", outputPath, "-y",
512
- ], { timeout: 600000, maxBuffer: 50 * 1024 * 1024 });
513
- }
514
- else {
515
- // Remotion fallback: embed video + captions in a single Remotion composition
516
- console.log("FFmpeg missing subtitles filter (libass) — using Remotion fallback...");
517
- const { generateCaptionComponent, renderWithEmbeddedVideo, ensureRemotionInstalled } = await import("../utils/remotion.js");
518
- const remotionErr = await ensureRemotionInstalled();
519
- if (remotionErr) {
520
- // Save SRT so the user still gets something
521
- const outputDir = dirname(outputPath);
522
- const outputSrtPath = join(outputDir, basename(outputPath, extname(outputPath)) + ".srt");
523
- await writeFile(outputSrtPath, srtContent);
524
- return { success: false, error: `${remotionErr}\nSRT saved to: ${outputSrtPath}` };
525
- }
526
- const videoDuration = await getVideoDuration(videoPath);
527
- const fps = 30;
528
- const durationInFrames = Math.ceil(videoDuration * fps);
529
- const videoFileName = "source_video.mp4";
530
- const { code, name } = generateCaptionComponent({
531
- segments: transcriptResult.segments.map((s) => ({
532
- start: s.startTime,
533
- end: s.endTime,
534
- text: s.text,
535
- })),
536
- style,
537
- fontSize,
538
- fontColor,
539
- position,
540
- width,
541
- height,
542
- videoFileName,
543
- });
544
- const renderResult = await renderWithEmbeddedVideo({
545
- componentCode: code,
546
- componentName: name,
547
- width,
548
- height,
549
- fps,
550
- durationInFrames,
551
- videoPath,
552
- videoFileName,
553
- outputPath,
554
- });
555
- if (!renderResult.success) {
556
- const outputDir = dirname(outputPath);
557
- const outputSrtPath = join(outputDir, basename(outputPath, extname(outputPath)) + ".srt");
558
- await writeFile(outputSrtPath, srtContent);
559
- return { success: false, error: `${renderResult.error}\nSRT saved to: ${outputSrtPath}` };
560
- }
561
- }
562
- // Copy SRT to output directory for user reference
563
- const outputDir = dirname(outputPath);
564
- const outputSrtPath = join(outputDir, basename(outputPath, extname(outputPath)) + ".srt");
565
- await writeFile(outputSrtPath, srtContent);
566
- return {
567
- success: true,
568
- outputPath,
569
- srtPath: outputSrtPath,
570
- segmentCount: transcriptResult.segments.length,
571
- };
572
- }
573
- finally {
574
- // Cleanup temp files
575
- try {
576
- const { rm } = await import("node:fs/promises");
577
- await rm(tmpDir, { recursive: true, force: true });
578
- }
579
- catch {
580
- // Ignore cleanup errors
581
- }
582
- }
583
- }
584
- catch (error) {
585
- return {
586
- success: false,
587
- error: `Caption failed: ${error instanceof Error ? error.message : String(error)}`,
588
- };
589
- }
590
- }
591
- /**
592
- * Reduce audio noise in a video or audio file using FFmpeg afftdn filter.
593
- *
594
- * Supports three strength presets (low/medium/high) with optional highpass/lowpass
595
- * for the "high" setting. Video streams are copied without re-encoding.
596
- *
597
- * @param options - Noise reduction configuration
598
- * @returns Result with output path and input duration
599
- */
600
- export async function executeNoiseReduce(options) {
601
- const { inputPath, outputPath, strength = "medium", noiseFloor, } = options;
602
- if (!existsSync(inputPath)) {
603
- return { success: false, error: `File not found: ${inputPath}` };
604
- }
605
- if (!commandExists("ffmpeg")) {
606
- return { success: false, error: "FFmpeg not found. Please install FFmpeg." };
607
- }
608
- try {
609
- const inputDuration = await getVideoDuration(inputPath);
610
- // Map strength to noise floor dB value
611
- const nf = noiseFloor ?? (strength === "low" ? -20 : strength === "high" ? -35 : -25);
612
- // Build audio filter
613
- let audioFilter = `afftdn=nf=${nf}`;
614
- if (strength === "high") {
615
- audioFilter = `${audioFilter},highpass=f=80,lowpass=f=12000`;
616
- }
617
- // Check if input has video stream
618
- let hasVideo = false;
619
- try {
620
- const { stdout } = await execSafe("ffprobe", [
621
- "-v", "error", "-select_streams", "v", "-show_entries", "stream=codec_type", "-of", "csv=p=0", inputPath,
622
- ], { maxBuffer: 10 * 1024 * 1024 });
623
- hasVideo = stdout.trim().includes("video");
624
- }
625
- catch {
626
- // No video stream
627
- }
628
- const args = ["-i", inputPath, "-af", audioFilter];
629
- if (hasVideo)
630
- args.push("-c:v", "copy");
631
- args.push(outputPath, "-y");
632
- await execSafe("ffmpeg", args, { timeout: 600000, maxBuffer: 50 * 1024 * 1024 });
633
- return {
634
- success: true,
635
- outputPath,
636
- inputDuration,
637
- };
638
- }
639
- catch (error) {
640
- return {
641
- success: false,
642
- error: `Noise reduction failed: ${error instanceof Error ? error.message : String(error)}`,
643
- };
644
- }
645
- }
646
- /**
647
- * Apply fade-in and/or fade-out effects to video and/or audio using FFmpeg.
648
- *
649
- * @param options - Fade configuration
650
- * @returns Result with output path and which fades were applied
651
- */
652
- export async function executeFade(options) {
653
- const { videoPath, outputPath, fadeIn = 1, fadeOut = 1, audioOnly = false, videoOnly = false, } = options;
654
- if (!existsSync(videoPath)) {
655
- return { success: false, error: `Video not found: ${videoPath}` };
656
- }
657
- if (!commandExists("ffmpeg")) {
658
- return { success: false, error: "FFmpeg not found. Please install FFmpeg." };
659
- }
660
- try {
661
- const totalDuration = await getVideoDuration(videoPath);
662
- const videoFilters = [];
663
- const audioFilters = [];
664
- // Video fade filters
665
- if (!audioOnly) {
666
- if (fadeIn > 0) {
667
- videoFilters.push(`fade=t=in:st=0:d=${fadeIn}`);
668
- }
669
- if (fadeOut > 0) {
670
- const fadeOutStart = Math.max(0, totalDuration - fadeOut);
671
- videoFilters.push(`fade=t=out:st=${fadeOutStart}:d=${fadeOut}`);
672
- }
673
- }
674
- // Audio fade filters
675
- if (!videoOnly) {
676
- if (fadeIn > 0) {
677
- audioFilters.push(`afade=t=in:st=0:d=${fadeIn}`);
678
- }
679
- if (fadeOut > 0) {
680
- const fadeOutStart = Math.max(0, totalDuration - fadeOut);
681
- audioFilters.push(`afade=t=out:st=${fadeOutStart}:d=${fadeOut}`);
682
- }
683
- }
684
- // Build FFmpeg command
685
- const ffmpegArgs = ["-i", videoPath];
686
- if (videoFilters.length > 0) {
687
- ffmpegArgs.push("-vf", videoFilters.join(","));
688
- }
689
- else if (audioOnly) {
690
- ffmpegArgs.push("-c:v", "copy");
691
- }
692
- if (audioFilters.length > 0) {
693
- ffmpegArgs.push("-af", audioFilters.join(","));
694
- }
695
- else if (videoOnly) {
696
- ffmpegArgs.push("-c:a", "copy");
697
- }
698
- ffmpegArgs.push(outputPath, "-y");
699
- await execSafe("ffmpeg", ffmpegArgs, { timeout: 600000, maxBuffer: 50 * 1024 * 1024 });
700
- return {
701
- success: true,
702
- outputPath,
703
- totalDuration,
704
- fadeInApplied: fadeIn > 0,
705
- fadeOutApplied: fadeOut > 0,
706
- };
707
- }
708
- catch (error) {
709
- return {
710
- success: false,
711
- error: `Fade failed: ${error instanceof Error ? error.message : String(error)}`,
712
- };
713
- }
714
- }
715
- /**
716
- * Translate an SRT subtitle file to a target language using Claude or OpenAI.
717
- *
718
- * Segments are batched (~30 at a time) for efficient API usage. Preserves
719
- * original timestamps; only text content is translated.
720
- *
721
- * @param options - Translation configuration
722
- * @returns Result with output path and segment count
723
- */
724
- export async function executeTranslateSrt(options) {
725
- const { srtPath, outputPath, targetLanguage, provider = "claude", sourceLanguage, apiKey, } = options;
726
- if (!existsSync(srtPath)) {
727
- return { success: false, error: `SRT file not found: ${srtPath}` };
728
- }
729
- try {
730
- const srtContent = await readFile(srtPath, "utf-8");
731
- const segments = parseSRT(srtContent);
732
- if (segments.length === 0) {
733
- return { success: false, error: "No subtitle segments found in SRT file" };
734
- }
735
- // Batch translate segments (~30 at a time)
736
- const batchSize = 30;
737
- const translatedSegments = [];
738
- for (let i = 0; i < segments.length; i += batchSize) {
739
- const batch = segments.slice(i, i + batchSize);
740
- const textsToTranslate = batch.map((s, idx) => `[${idx}] ${s.text}`).join("\n");
741
- const translatePrompt = `Translate the following subtitle texts to ${targetLanguage}.` +
742
- (sourceLanguage ? ` The source language is ${sourceLanguage}.` : "") +
743
- ` Return ONLY the translated texts, one per line, preserving the [N] prefix format exactly. ` +
744
- `Do not add explanations.\n\n${textsToTranslate}`;
745
- let translatedText;
746
- if (provider === "openai") {
747
- const openaiKey = apiKey || process.env.OPENAI_API_KEY;
748
- if (!openaiKey) {
749
- return { success: false, error: "OpenAI API key required for translation. Run 'vibe setup' or set OPENAI_API_KEY in .env" };
750
- }
751
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
752
- method: "POST",
753
- headers: {
754
- "Content-Type": "application/json",
755
- Authorization: `Bearer ${openaiKey}`,
756
- },
757
- body: JSON.stringify({
758
- model: "gpt-5-mini",
759
- messages: [{ role: "user", content: translatePrompt }],
760
- temperature: 0.3,
761
- }),
762
- });
763
- if (!response.ok) {
764
- return { success: false, error: `OpenAI API error: ${response.status} ${response.statusText}` };
765
- }
766
- const data = await response.json();
767
- translatedText = data.choices[0]?.message?.content || "";
768
- }
769
- else {
770
- const claudeKey = apiKey || process.env.ANTHROPIC_API_KEY;
771
- if (!claudeKey) {
772
- return { success: false, error: "Anthropic API key required for translation. Run 'vibe setup' or set ANTHROPIC_API_KEY in .env" };
773
- }
774
- const response = await fetch("https://api.anthropic.com/v1/messages", {
775
- method: "POST",
776
- headers: {
777
- "Content-Type": "application/json",
778
- "x-api-key": claudeKey,
779
- "anthropic-version": "2023-06-01",
780
- },
781
- body: JSON.stringify({
782
- model: "claude-sonnet-4-6-20250514",
783
- max_tokens: 4096,
784
- messages: [{ role: "user", content: translatePrompt }],
785
- }),
786
- });
787
- if (!response.ok) {
788
- return { success: false, error: `Claude API error: ${response.status} ${response.statusText}` };
789
- }
790
- const data = await response.json();
791
- translatedText = data.content?.find((c) => c.type === "text")?.text || "";
792
- }
793
- // Parse translated lines
794
- const translatedLines = translatedText.trim().split("\n");
795
- for (let j = 0; j < batch.length; j++) {
796
- const seg = batch[j];
797
- // Try to match [N] prefix
798
- const line = translatedLines[j];
799
- let text;
800
- if (line) {
801
- text = line.replace(/^\[\d+\]\s*/, "").trim();
802
- }
803
- else {
804
- // Fallback: use original text if translation is missing
805
- text = seg.text;
806
- }
807
- translatedSegments.push({
808
- startTime: seg.startTime,
809
- endTime: seg.endTime,
810
- text,
811
- });
812
- }
813
- }
814
- // Format as SRT and write
815
- const translatedSrt = formatSRT(translatedSegments);
816
- await writeFile(outputPath, translatedSrt);
817
- return {
818
- success: true,
819
- outputPath,
820
- segmentCount: translatedSegments.length,
821
- sourceLanguage: sourceLanguage || "auto",
822
- targetLanguage,
823
- };
824
- }
825
- catch (error) {
826
- return {
827
- success: false,
828
- error: `Translation failed: ${error instanceof Error ? error.message : String(error)}`,
829
- };
830
- }
831
- }
832
- /**
833
- * Detect system font path for FFmpeg drawtext
834
- */
835
- function detectSystemFont() {
836
- const platform = process.platform;
837
- if (platform === "darwin") {
838
- const candidates = [
839
- "/System/Library/Fonts/Helvetica.ttc",
840
- "/System/Library/Fonts/HelveticaNeue.ttc",
841
- "/Library/Fonts/Arial.ttf",
842
- ];
843
- for (const f of candidates) {
844
- if (existsSync(f))
845
- return f;
846
- }
847
- }
848
- else if (platform === "linux") {
849
- const candidates = [
850
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
851
- "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
852
- "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
853
- ];
854
- for (const f of candidates) {
855
- if (existsSync(f))
856
- return f;
857
- }
858
- }
859
- else if (platform === "win32") {
860
- const candidates = [
861
- "C:\\Windows\\Fonts\\arial.ttf",
862
- "C:\\Windows\\Fonts\\segoeui.ttf",
863
- ];
864
- for (const f of candidates) {
865
- if (existsSync(f))
866
- return f;
867
- }
868
- }
869
- return null;
870
- }
871
- /**
872
- * Get video resolution via ffprobe
873
- */
874
- async function getVideoResolution(videoPath) {
875
- const { stdout } = await execSafe("ffprobe", [
876
- "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=p=0", videoPath,
877
- ]);
878
- const [w, h] = stdout.trim().split(",").map(Number);
879
- return { width: w || 1920, height: h || 1080 };
880
- }
881
- /**
882
- * Escape text for FFmpeg drawtext filter
883
- */
884
- function escapeDrawtext(text) {
885
- return text
886
- .replace(/\\/g, "\\\\\\\\")
887
- .replace(/'/g, "'\\\\\\''")
888
- .replace(/:/g, "\\\\:")
889
- .replace(/%/g, "\\\\%");
890
- }
891
- /**
892
- * Apply text overlays to a video using FFmpeg drawtext filter.
893
- *
894
- * Supports multiple text lines with configurable style, position, font,
895
- * and fade-in/out. Auto-detects system fonts across macOS, Linux, and Windows.
896
- *
897
- * @param options - Text overlay configuration
898
- * @returns Result with absolute output path
899
- */
900
- export async function applyTextOverlays(options) {
901
- const { videoPath, texts, outputPath, style = "lower-third", fontSize: customFontSize, fontColor = "white", fadeDuration = 0.3, startTime = 0, } = options;
902
- if (!texts || texts.length === 0) {
903
- return { success: false, error: "No texts provided" };
904
- }
905
- const absVideoPath = resolve(process.cwd(), videoPath);
906
- const absOutputPath = resolve(process.cwd(), outputPath);
907
- // Check video exists
908
- if (!existsSync(absVideoPath)) {
909
- return { success: false, error: `Video not found: ${absVideoPath}` };
910
- }
911
- // Check FFmpeg
912
- if (!commandExists("ffmpeg")) {
913
- return { success: false, error: "FFmpeg not found. Please install FFmpeg." };
914
- }
915
- // Check drawtext filter availability
916
- let hasDrawtext = true;
917
- try {
918
- const { stdout } = await execSafe("ffmpeg", ["-filters"]);
919
- hasDrawtext = stdout.includes("drawtext");
920
- }
921
- catch {
922
- // If filter check fails, assume available and let FFmpeg error naturally
923
- }
924
- if (!hasDrawtext) {
925
- // Remotion fallback: render text overlay without libfreetype
926
- console.log("FFmpeg missing drawtext filter (libfreetype) — using Remotion fallback...");
927
- const { generateTextOverlayComponent, renderWithEmbeddedVideo, ensureRemotionInstalled } = await import("../utils/remotion.js");
928
- const remotionErr = await ensureRemotionInstalled();
929
- if (remotionErr) {
930
- const platform = process.platform;
931
- let hint = "";
932
- if (platform === "darwin") {
933
- hint = "\n\nFix: brew uninstall ffmpeg && brew install ffmpeg\n(The default homebrew formula includes libfreetype)";
934
- }
935
- else if (platform === "linux") {
936
- hint = "\n\nFix: sudo apt install ffmpeg (Ubuntu/Debian)\n or rebuild FFmpeg with --enable-libfreetype";
937
- }
938
- return {
939
- success: false,
940
- error: `FFmpeg 'drawtext' filter not available and Remotion fallback unavailable.\n${remotionErr}${hint}`,
941
- };
942
- }
943
- const { width, height } = await getVideoResolution(absVideoPath);
944
- const videoDuration = await getVideoDuration(absVideoPath);
945
- const baseFontSize = customFontSize || Math.round(height / 20);
946
- const endTime = options.endTime ?? videoDuration;
947
- const fps = 30;
948
- const durationInFrames = Math.ceil(videoDuration * fps);
949
- const videoFileName = "source_video.mp4";
950
- const { code, name } = generateTextOverlayComponent({
951
- texts,
952
- style,
953
- fontSize: baseFontSize,
954
- fontColor,
955
- startTime,
956
- endTime,
957
- fadeDuration,
958
- width,
959
- height,
960
- videoFileName,
961
- });
962
- const renderResult = await renderWithEmbeddedVideo({
963
- componentCode: code,
964
- componentName: name,
965
- width,
966
- height,
967
- fps,
968
- durationInFrames,
969
- videoPath: absVideoPath,
970
- videoFileName,
971
- outputPath: absOutputPath,
972
- });
973
- return renderResult.success
974
- ? { success: true, outputPath: renderResult.outputPath || absOutputPath }
975
- : { success: false, error: renderResult.error || "Remotion render failed" };
976
- }
977
- // Get video resolution for scaling
978
- const { width, height } = await getVideoResolution(absVideoPath);
979
- const baseFontSize = customFontSize || Math.round(height / 20);
980
- // Get video duration for endTime default
981
- const videoDuration = await getVideoDuration(absVideoPath);
982
- const endTime = options.endTime ?? videoDuration;
983
- // Detect font
984
- const fontPath = detectSystemFont();
985
- const fontFile = fontPath ? `fontfile=${fontPath}:` : "";
986
- // Build drawtext filters based on style
987
- const filters = [];
988
- for (let i = 0; i < texts.length; i++) {
989
- const escaped = escapeDrawtext(texts[i]);
990
- let x;
991
- let y;
992
- let fs;
993
- let fc = fontColor;
994
- let boxEnabled = 0;
995
- let boxColor = "black@0.5";
996
- let borderW = 0;
997
- switch (style) {
998
- case "center-bold":
999
- x = "(w-text_w)/2";
1000
- y = `(h-text_h)/2+${i * Math.round(baseFontSize * 1.4)}`;
1001
- fs = Math.round(baseFontSize * 1.5);
1002
- borderW = 3;
1003
- break;
1004
- case "subtitle":
1005
- x = "(w-text_w)/2";
1006
- y = `h-${Math.round(height * 0.12)}+${i * Math.round(baseFontSize * 1.3)}`;
1007
- fs = baseFontSize;
1008
- boxEnabled = 1;
1009
- boxColor = "black@0.6";
1010
- break;
1011
- case "minimal":
1012
- x = `${Math.round(width * 0.05)}`;
1013
- y = `${Math.round(height * 0.05)}+${i * Math.round(baseFontSize * 1.3)}`;
1014
- fs = Math.round(baseFontSize * 0.8);
1015
- fc = "white@0.85";
1016
- break;
1017
- case "lower-third":
1018
- default:
1019
- x = `${Math.round(width * 0.05)}`;
1020
- y = `h-${Math.round(height * 0.18)}+${i * Math.round(baseFontSize * 1.3)}`;
1021
- fs = i === 0 ? Math.round(baseFontSize * 1.2) : baseFontSize;
1022
- boxEnabled = 1;
1023
- boxColor = "black@0.5";
1024
- break;
1025
- }
1026
- // Build alpha expression for fade in/out
1027
- const fadeIn = `if(lt(t-${startTime}\\,${fadeDuration})\\,(t-${startTime})/${fadeDuration}\\,1)`;
1028
- const fadeOut = `if(gt(t\\,${endTime - fadeDuration})\\,( ${endTime}-t)/${fadeDuration}\\,1)`;
1029
- const alpha = `min(${fadeIn}\\,${fadeOut})`;
1030
- let filter = `drawtext=${fontFile}text='${escaped}':fontsize=${fs}:fontcolor=${fc}:x=${x}:y=${y}:borderw=${borderW}:enable='between(t\\,${startTime}\\,${endTime})'`;
1031
- filter += `:alpha='${alpha}'`;
1032
- if (boxEnabled) {
1033
- filter += `:box=1:boxcolor=${boxColor}:boxborderw=8`;
1034
- }
1035
- filters.push(filter);
1036
- }
1037
- const filterChain = filters.join(",");
1038
- try {
1039
- await execSafe("ffmpeg", [
1040
- "-i", absVideoPath, "-vf", filterChain, "-c:a", "copy", absOutputPath, "-y",
1041
- ], { timeout: 600000, maxBuffer: 50 * 1024 * 1024 });
1042
- return { success: true, outputPath: absOutputPath };
1043
- }
1044
- catch (error) {
1045
- return {
1046
- success: false,
1047
- error: `FFmpeg failed: ${error instanceof Error ? error.message : String(error)}`,
1048
- };
1049
- }
1050
- }
1051
- /**
1052
- * Execute text overlay for CLI/Agent usage. Delegates to {@link applyTextOverlays}.
1053
- *
1054
- * @param options - Text overlay configuration
1055
- * @returns Result with absolute output path
1056
- */
1057
- export async function executeTextOverlay(options) {
1058
- return applyTextOverlays(options);
1059
- }
1060
- //# sourceMappingURL=ai-edit.js.map