@vibeframe/cli 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (420) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-lint.log +21 -0
  3. package/.turbo/turbo-test.log +689 -0
  4. package/dist/agent/adapters/claude.d.ts +15 -0
  5. package/dist/agent/adapters/claude.d.ts.map +1 -0
  6. package/dist/agent/adapters/claude.js +119 -0
  7. package/dist/agent/adapters/claude.js.map +1 -0
  8. package/dist/agent/adapters/gemini.d.ts +15 -0
  9. package/dist/agent/adapters/gemini.d.ts.map +1 -0
  10. package/dist/agent/adapters/gemini.js +132 -0
  11. package/dist/agent/adapters/gemini.js.map +1 -0
  12. package/dist/agent/adapters/index.d.ts +27 -0
  13. package/dist/agent/adapters/index.d.ts.map +1 -0
  14. package/dist/agent/adapters/index.js +38 -0
  15. package/dist/agent/adapters/index.js.map +1 -0
  16. package/dist/agent/adapters/ollama.d.ts +20 -0
  17. package/dist/agent/adapters/ollama.d.ts.map +1 -0
  18. package/dist/agent/adapters/ollama.js +186 -0
  19. package/dist/agent/adapters/ollama.js.map +1 -0
  20. package/dist/agent/adapters/openai.d.ts +15 -0
  21. package/dist/agent/adapters/openai.d.ts.map +1 -0
  22. package/dist/agent/adapters/openai.js +92 -0
  23. package/dist/agent/adapters/openai.js.map +1 -0
  24. package/dist/agent/adapters/xai.d.ts +15 -0
  25. package/dist/agent/adapters/xai.d.ts.map +1 -0
  26. package/dist/agent/adapters/xai.js +95 -0
  27. package/dist/agent/adapters/xai.js.map +1 -0
  28. package/dist/agent/index.d.ts +69 -0
  29. package/dist/agent/index.d.ts.map +1 -0
  30. package/dist/agent/index.js +180 -0
  31. package/dist/agent/index.js.map +1 -0
  32. package/dist/agent/memory/index.d.ts +70 -0
  33. package/dist/agent/memory/index.d.ts.map +1 -0
  34. package/dist/agent/memory/index.js +132 -0
  35. package/dist/agent/memory/index.js.map +1 -0
  36. package/dist/agent/prompts/system.d.ts +6 -0
  37. package/dist/agent/prompts/system.d.ts.map +1 -0
  38. package/dist/agent/prompts/system.js +103 -0
  39. package/dist/agent/prompts/system.js.map +1 -0
  40. package/dist/agent/tools/ai-editing.d.ts +15 -0
  41. package/dist/agent/tools/ai-editing.d.ts.map +1 -0
  42. package/dist/agent/tools/ai-editing.js +763 -0
  43. package/dist/agent/tools/ai-editing.js.map +1 -0
  44. package/dist/agent/tools/ai-generation.d.ts +13 -0
  45. package/dist/agent/tools/ai-generation.d.ts.map +1 -0
  46. package/dist/agent/tools/ai-generation.js +973 -0
  47. package/dist/agent/tools/ai-generation.js.map +1 -0
  48. package/dist/agent/tools/ai-pipeline.d.ts +14 -0
  49. package/dist/agent/tools/ai-pipeline.d.ts.map +1 -0
  50. package/dist/agent/tools/ai-pipeline.js +961 -0
  51. package/dist/agent/tools/ai-pipeline.js.map +1 -0
  52. package/dist/agent/tools/ai.d.ts +13 -0
  53. package/dist/agent/tools/ai.d.ts.map +1 -0
  54. package/dist/agent/tools/ai.js +19 -0
  55. package/dist/agent/tools/ai.js.map +1 -0
  56. package/dist/agent/tools/batch.d.ts +6 -0
  57. package/dist/agent/tools/batch.d.ts.map +1 -0
  58. package/dist/agent/tools/batch.js +383 -0
  59. package/dist/agent/tools/batch.js.map +1 -0
  60. package/dist/agent/tools/e2e.test.d.ts +26 -0
  61. package/dist/agent/tools/e2e.test.d.ts.map +1 -0
  62. package/dist/agent/tools/e2e.test.js +397 -0
  63. package/dist/agent/tools/e2e.test.js.map +1 -0
  64. package/dist/agent/tools/export.d.ts +6 -0
  65. package/dist/agent/tools/export.d.ts.map +1 -0
  66. package/dist/agent/tools/export.js +171 -0
  67. package/dist/agent/tools/export.js.map +1 -0
  68. package/dist/agent/tools/filesystem.d.ts +6 -0
  69. package/dist/agent/tools/filesystem.d.ts.map +1 -0
  70. package/dist/agent/tools/filesystem.js +212 -0
  71. package/dist/agent/tools/filesystem.js.map +1 -0
  72. package/dist/agent/tools/index.d.ts +65 -0
  73. package/dist/agent/tools/index.d.ts.map +1 -0
  74. package/dist/agent/tools/index.js +120 -0
  75. package/dist/agent/tools/index.js.map +1 -0
  76. package/dist/agent/tools/integration.test.d.ts +11 -0
  77. package/dist/agent/tools/integration.test.d.ts.map +1 -0
  78. package/dist/agent/tools/integration.test.js +659 -0
  79. package/dist/agent/tools/integration.test.js.map +1 -0
  80. package/dist/agent/tools/media.d.ts +6 -0
  81. package/dist/agent/tools/media.d.ts.map +1 -0
  82. package/dist/agent/tools/media.js +616 -0
  83. package/dist/agent/tools/media.js.map +1 -0
  84. package/dist/agent/tools/project.d.ts +6 -0
  85. package/dist/agent/tools/project.d.ts.map +1 -0
  86. package/dist/agent/tools/project.js +284 -0
  87. package/dist/agent/tools/project.js.map +1 -0
  88. package/dist/agent/tools/timeline.d.ts +6 -0
  89. package/dist/agent/tools/timeline.d.ts.map +1 -0
  90. package/dist/agent/tools/timeline.js +873 -0
  91. package/dist/agent/tools/timeline.js.map +1 -0
  92. package/dist/agent/types.d.ts +59 -0
  93. package/dist/agent/types.d.ts.map +1 -0
  94. package/dist/agent/types.js +5 -0
  95. package/dist/agent/types.js.map +1 -0
  96. package/dist/commands/agent.d.ts +21 -0
  97. package/dist/commands/agent.d.ts.map +1 -0
  98. package/dist/commands/agent.js +290 -0
  99. package/dist/commands/agent.js.map +1 -0
  100. package/dist/commands/ai-analyze.d.ts +106 -0
  101. package/dist/commands/ai-analyze.d.ts.map +1 -0
  102. package/dist/commands/ai-analyze.js +327 -0
  103. package/dist/commands/ai-analyze.js.map +1 -0
  104. package/dist/commands/ai-animated-caption.d.ts +64 -0
  105. package/dist/commands/ai-animated-caption.d.ts.map +1 -0
  106. package/dist/commands/ai-animated-caption.js +272 -0
  107. package/dist/commands/ai-animated-caption.js.map +1 -0
  108. package/dist/commands/ai-audio.d.ts +20 -0
  109. package/dist/commands/ai-audio.d.ts.map +1 -0
  110. package/dist/commands/ai-audio.js +808 -0
  111. package/dist/commands/ai-audio.js.map +1 -0
  112. package/dist/commands/ai-broll.d.ts +15 -0
  113. package/dist/commands/ai-broll.d.ts.map +1 -0
  114. package/dist/commands/ai-broll.js +406 -0
  115. package/dist/commands/ai-broll.js.map +1 -0
  116. package/dist/commands/ai-edit-cli.d.ts +14 -0
  117. package/dist/commands/ai-edit-cli.d.ts.map +1 -0
  118. package/dist/commands/ai-edit-cli.js +579 -0
  119. package/dist/commands/ai-edit-cli.js.map +1 -0
  120. package/dist/commands/ai-edit.d.ts +398 -0
  121. package/dist/commands/ai-edit.d.ts.map +1 -0
  122. package/dist/commands/ai-edit.js +1019 -0
  123. package/dist/commands/ai-edit.js.map +1 -0
  124. package/dist/commands/ai-fill-gaps.d.ts +14 -0
  125. package/dist/commands/ai-fill-gaps.d.ts.map +1 -0
  126. package/dist/commands/ai-fill-gaps.js +451 -0
  127. package/dist/commands/ai-fill-gaps.js.map +1 -0
  128. package/dist/commands/ai-helpers.d.ts +20 -0
  129. package/dist/commands/ai-helpers.d.ts.map +1 -0
  130. package/dist/commands/ai-helpers.js +59 -0
  131. package/dist/commands/ai-helpers.js.map +1 -0
  132. package/dist/commands/ai-highlights.d.ts +127 -0
  133. package/dist/commands/ai-highlights.d.ts.map +1 -0
  134. package/dist/commands/ai-highlights.js +1026 -0
  135. package/dist/commands/ai-highlights.js.map +1 -0
  136. package/dist/commands/ai-image.d.ts +34 -0
  137. package/dist/commands/ai-image.d.ts.map +1 -0
  138. package/dist/commands/ai-image.js +653 -0
  139. package/dist/commands/ai-image.js.map +1 -0
  140. package/dist/commands/ai-motion.d.ts +50 -0
  141. package/dist/commands/ai-motion.d.ts.map +1 -0
  142. package/dist/commands/ai-motion.js +271 -0
  143. package/dist/commands/ai-motion.js.map +1 -0
  144. package/dist/commands/ai-narrate.d.ts +66 -0
  145. package/dist/commands/ai-narrate.d.ts.map +1 -0
  146. package/dist/commands/ai-narrate.js +329 -0
  147. package/dist/commands/ai-narrate.js.map +1 -0
  148. package/dist/commands/ai-review.d.ts +57 -0
  149. package/dist/commands/ai-review.d.ts.map +1 -0
  150. package/dist/commands/ai-review.js +251 -0
  151. package/dist/commands/ai-review.js.map +1 -0
  152. package/dist/commands/ai-script-pipeline-cli.d.ts +9 -0
  153. package/dist/commands/ai-script-pipeline-cli.d.ts.map +1 -0
  154. package/dist/commands/ai-script-pipeline-cli.js +1494 -0
  155. package/dist/commands/ai-script-pipeline-cli.js.map +1 -0
  156. package/dist/commands/ai-script-pipeline.d.ts +259 -0
  157. package/dist/commands/ai-script-pipeline.d.ts.map +1 -0
  158. package/dist/commands/ai-script-pipeline.js +1027 -0
  159. package/dist/commands/ai-script-pipeline.js.map +1 -0
  160. package/dist/commands/ai-suggest-edit.d.ts +14 -0
  161. package/dist/commands/ai-suggest-edit.d.ts.map +1 -0
  162. package/dist/commands/ai-suggest-edit.js +220 -0
  163. package/dist/commands/ai-suggest-edit.js.map +1 -0
  164. package/dist/commands/ai-video-fx.d.ts +14 -0
  165. package/dist/commands/ai-video-fx.d.ts.map +1 -0
  166. package/dist/commands/ai-video-fx.js +395 -0
  167. package/dist/commands/ai-video-fx.js.map +1 -0
  168. package/dist/commands/ai-video.d.ts +15 -0
  169. package/dist/commands/ai-video.d.ts.map +1 -0
  170. package/dist/commands/ai-video.js +785 -0
  171. package/dist/commands/ai-video.js.map +1 -0
  172. package/dist/commands/ai-viral.d.ts +15 -0
  173. package/dist/commands/ai-viral.d.ts.map +1 -0
  174. package/dist/commands/ai-viral.js +519 -0
  175. package/dist/commands/ai-viral.js.map +1 -0
  176. package/dist/commands/ai-visual-fx.d.ts +14 -0
  177. package/dist/commands/ai-visual-fx.d.ts.map +1 -0
  178. package/dist/commands/ai-visual-fx.js +505 -0
  179. package/dist/commands/ai-visual-fx.js.map +1 -0
  180. package/dist/commands/ai.d.ts +38 -0
  181. package/dist/commands/ai.d.ts.map +1 -0
  182. package/dist/commands/ai.js +225 -0
  183. package/dist/commands/ai.js.map +1 -0
  184. package/dist/commands/ai.test.d.ts +2 -0
  185. package/dist/commands/ai.test.d.ts.map +1 -0
  186. package/dist/commands/ai.test.js +554 -0
  187. package/dist/commands/ai.test.js.map +1 -0
  188. package/dist/commands/analyze.d.ts +16 -0
  189. package/dist/commands/analyze.d.ts.map +1 -0
  190. package/dist/commands/analyze.js +247 -0
  191. package/dist/commands/analyze.js.map +1 -0
  192. package/dist/commands/audio.d.ts +18 -0
  193. package/dist/commands/audio.d.ts.map +1 -0
  194. package/dist/commands/audio.js +539 -0
  195. package/dist/commands/audio.js.map +1 -0
  196. package/dist/commands/batch.d.ts +3 -0
  197. package/dist/commands/batch.d.ts.map +1 -0
  198. package/dist/commands/batch.js +366 -0
  199. package/dist/commands/batch.js.map +1 -0
  200. package/dist/commands/batch.test.d.ts +2 -0
  201. package/dist/commands/batch.test.d.ts.map +1 -0
  202. package/dist/commands/batch.test.js +203 -0
  203. package/dist/commands/batch.test.js.map +1 -0
  204. package/dist/commands/detect.d.ts +3 -0
  205. package/dist/commands/detect.d.ts.map +1 -0
  206. package/dist/commands/detect.js +273 -0
  207. package/dist/commands/detect.js.map +1 -0
  208. package/dist/commands/doctor.d.ts +6 -0
  209. package/dist/commands/doctor.d.ts.map +1 -0
  210. package/dist/commands/doctor.js +191 -0
  211. package/dist/commands/doctor.js.map +1 -0
  212. package/dist/commands/edit-cmd.d.ts +26 -0
  213. package/dist/commands/edit-cmd.d.ts.map +1 -0
  214. package/dist/commands/edit-cmd.js +870 -0
  215. package/dist/commands/edit-cmd.js.map +1 -0
  216. package/dist/commands/export.d.ts +39 -0
  217. package/dist/commands/export.d.ts.map +1 -0
  218. package/dist/commands/export.js +730 -0
  219. package/dist/commands/export.js.map +1 -0
  220. package/dist/commands/generate.d.ts +25 -0
  221. package/dist/commands/generate.d.ts.map +1 -0
  222. package/dist/commands/generate.js +1885 -0
  223. package/dist/commands/generate.js.map +1 -0
  224. package/dist/commands/media.d.ts +3 -0
  225. package/dist/commands/media.d.ts.map +1 -0
  226. package/dist/commands/media.js +165 -0
  227. package/dist/commands/media.js.map +1 -0
  228. package/dist/commands/output.d.ts +45 -0
  229. package/dist/commands/output.d.ts.map +1 -0
  230. package/dist/commands/output.js +122 -0
  231. package/dist/commands/output.js.map +1 -0
  232. package/dist/commands/pipeline.d.ts +19 -0
  233. package/dist/commands/pipeline.d.ts.map +1 -0
  234. package/dist/commands/pipeline.js +345 -0
  235. package/dist/commands/pipeline.js.map +1 -0
  236. package/dist/commands/project.d.ts +3 -0
  237. package/dist/commands/project.d.ts.map +1 -0
  238. package/dist/commands/project.js +139 -0
  239. package/dist/commands/project.js.map +1 -0
  240. package/dist/commands/project.test.d.ts +2 -0
  241. package/dist/commands/project.test.d.ts.map +1 -0
  242. package/dist/commands/project.test.js +105 -0
  243. package/dist/commands/project.test.js.map +1 -0
  244. package/dist/commands/sanitize.d.ts +21 -0
  245. package/dist/commands/sanitize.d.ts.map +1 -0
  246. package/dist/commands/sanitize.js +56 -0
  247. package/dist/commands/sanitize.js.map +1 -0
  248. package/dist/commands/schema.d.ts +11 -0
  249. package/dist/commands/schema.d.ts.map +1 -0
  250. package/dist/commands/schema.js +101 -0
  251. package/dist/commands/schema.js.map +1 -0
  252. package/dist/commands/setup.d.ts +6 -0
  253. package/dist/commands/setup.d.ts.map +1 -0
  254. package/dist/commands/setup.js +440 -0
  255. package/dist/commands/setup.js.map +1 -0
  256. package/dist/commands/timeline.d.ts +3 -0
  257. package/dist/commands/timeline.d.ts.map +1 -0
  258. package/dist/commands/timeline.js +469 -0
  259. package/dist/commands/timeline.js.map +1 -0
  260. package/dist/commands/timeline.test.d.ts +2 -0
  261. package/dist/commands/timeline.test.d.ts.map +1 -0
  262. package/dist/commands/timeline.test.js +320 -0
  263. package/dist/commands/timeline.test.js.map +1 -0
  264. package/dist/commands/validate.d.ts +32 -0
  265. package/dist/commands/validate.d.ts.map +1 -0
  266. package/dist/commands/validate.js +63 -0
  267. package/dist/commands/validate.js.map +1 -0
  268. package/dist/config/config.test.d.ts +2 -0
  269. package/dist/config/config.test.d.ts.map +1 -0
  270. package/dist/config/config.test.js +164 -0
  271. package/dist/config/config.test.js.map +1 -0
  272. package/dist/config/index.d.ts +35 -0
  273. package/dist/config/index.d.ts.map +1 -0
  274. package/dist/config/index.js +101 -0
  275. package/dist/config/index.js.map +1 -0
  276. package/dist/config/schema.d.ts +43 -0
  277. package/dist/config/schema.d.ts.map +1 -0
  278. package/dist/config/schema.js +42 -0
  279. package/dist/config/schema.js.map +1 -0
  280. package/dist/engine/index.d.ts +3 -0
  281. package/dist/engine/index.d.ts.map +1 -0
  282. package/dist/engine/index.js +2 -0
  283. package/dist/engine/index.js.map +1 -0
  284. package/dist/engine/project.d.ts +84 -0
  285. package/dist/engine/project.d.ts.map +1 -0
  286. package/dist/engine/project.js +355 -0
  287. package/dist/engine/project.js.map +1 -0
  288. package/dist/engine/project.test.d.ts +2 -0
  289. package/dist/engine/project.test.d.ts.map +1 -0
  290. package/dist/engine/project.test.js +599 -0
  291. package/dist/engine/project.test.js.map +1 -0
  292. package/dist/index.d.ts +7 -0
  293. package/dist/index.d.ts.map +1 -0
  294. package/dist/index.js +131 -0
  295. package/dist/index.js.map +1 -0
  296. package/dist/utils/api-key.d.ts +36 -0
  297. package/dist/utils/api-key.d.ts.map +1 -0
  298. package/dist/utils/api-key.js +211 -0
  299. package/dist/utils/api-key.js.map +1 -0
  300. package/dist/utils/api-key.test.d.ts +2 -0
  301. package/dist/utils/api-key.test.d.ts.map +1 -0
  302. package/dist/utils/api-key.test.js +35 -0
  303. package/dist/utils/api-key.test.js.map +1 -0
  304. package/dist/utils/audio.d.ts +23 -0
  305. package/dist/utils/audio.d.ts.map +1 -0
  306. package/dist/utils/audio.js +79 -0
  307. package/dist/utils/audio.js.map +1 -0
  308. package/dist/utils/exec-safe.d.ts +22 -0
  309. package/dist/utils/exec-safe.d.ts.map +1 -0
  310. package/dist/utils/exec-safe.js +62 -0
  311. package/dist/utils/exec-safe.js.map +1 -0
  312. package/dist/utils/first-run.d.ts +13 -0
  313. package/dist/utils/first-run.d.ts.map +1 -0
  314. package/dist/utils/first-run.js +48 -0
  315. package/dist/utils/first-run.js.map +1 -0
  316. package/dist/utils/provider-resolver.d.ts +15 -0
  317. package/dist/utils/provider-resolver.d.ts.map +1 -0
  318. package/dist/utils/provider-resolver.js +42 -0
  319. package/dist/utils/provider-resolver.js.map +1 -0
  320. package/dist/utils/remotion.d.ts +210 -0
  321. package/dist/utils/remotion.d.ts.map +1 -0
  322. package/dist/utils/remotion.js +731 -0
  323. package/dist/utils/remotion.js.map +1 -0
  324. package/dist/utils/subtitle.d.ts +65 -0
  325. package/dist/utils/subtitle.d.ts.map +1 -0
  326. package/dist/utils/subtitle.js +135 -0
  327. package/dist/utils/subtitle.js.map +1 -0
  328. package/dist/utils/subtitle.test.d.ts +2 -0
  329. package/dist/utils/subtitle.test.d.ts.map +1 -0
  330. package/dist/utils/subtitle.test.js +175 -0
  331. package/dist/utils/subtitle.test.js.map +1 -0
  332. package/dist/utils/tty.d.ts +45 -0
  333. package/dist/utils/tty.d.ts.map +1 -0
  334. package/dist/utils/tty.js +172 -0
  335. package/dist/utils/tty.js.map +1 -0
  336. package/package.json +102 -0
  337. package/src/agent/adapters/claude.ts +143 -0
  338. package/src/agent/adapters/gemini.ts +159 -0
  339. package/src/agent/adapters/index.ts +61 -0
  340. package/src/agent/adapters/ollama.ts +231 -0
  341. package/src/agent/adapters/openai.ts +116 -0
  342. package/src/agent/adapters/xai.ts +119 -0
  343. package/src/agent/index.ts +251 -0
  344. package/src/agent/memory/index.ts +151 -0
  345. package/src/agent/prompts/system.ts +106 -0
  346. package/src/agent/tools/ai-editing.ts +845 -0
  347. package/src/agent/tools/ai-generation.ts +1073 -0
  348. package/src/agent/tools/ai-pipeline.ts +1055 -0
  349. package/src/agent/tools/ai.ts +21 -0
  350. package/src/agent/tools/batch.ts +429 -0
  351. package/src/agent/tools/e2e.test.ts +545 -0
  352. package/src/agent/tools/export.ts +184 -0
  353. package/src/agent/tools/filesystem.ts +237 -0
  354. package/src/agent/tools/index.ts +150 -0
  355. package/src/agent/tools/integration.test.ts +775 -0
  356. package/src/agent/tools/media.ts +697 -0
  357. package/src/agent/tools/project.ts +313 -0
  358. package/src/agent/tools/timeline.ts +951 -0
  359. package/src/agent/types.ts +68 -0
  360. package/src/commands/agent.ts +340 -0
  361. package/src/commands/ai-analyze.ts +429 -0
  362. package/src/commands/ai-animated-caption.ts +390 -0
  363. package/src/commands/ai-audio.ts +941 -0
  364. package/src/commands/ai-broll.ts +490 -0
  365. package/src/commands/ai-edit-cli.ts +658 -0
  366. package/src/commands/ai-edit.ts +1542 -0
  367. package/src/commands/ai-fill-gaps.ts +566 -0
  368. package/src/commands/ai-helpers.ts +65 -0
  369. package/src/commands/ai-highlights.ts +1303 -0
  370. package/src/commands/ai-image.ts +761 -0
  371. package/src/commands/ai-motion.ts +347 -0
  372. package/src/commands/ai-narrate.ts +451 -0
  373. package/src/commands/ai-review.ts +309 -0
  374. package/src/commands/ai-script-pipeline-cli.ts +1710 -0
  375. package/src/commands/ai-script-pipeline.ts +1365 -0
  376. package/src/commands/ai-suggest-edit.ts +264 -0
  377. package/src/commands/ai-video-fx.ts +445 -0
  378. package/src/commands/ai-video.ts +915 -0
  379. package/src/commands/ai-viral.ts +595 -0
  380. package/src/commands/ai-visual-fx.ts +601 -0
  381. package/src/commands/ai.test.ts +627 -0
  382. package/src/commands/ai.ts +307 -0
  383. package/src/commands/analyze.ts +282 -0
  384. package/src/commands/audio.ts +644 -0
  385. package/src/commands/batch.test.ts +279 -0
  386. package/src/commands/batch.ts +440 -0
  387. package/src/commands/detect.ts +329 -0
  388. package/src/commands/doctor.ts +237 -0
  389. package/src/commands/edit-cmd.ts +1014 -0
  390. package/src/commands/export.ts +918 -0
  391. package/src/commands/generate.ts +2146 -0
  392. package/src/commands/media.ts +177 -0
  393. package/src/commands/output.ts +142 -0
  394. package/src/commands/pipeline.ts +398 -0
  395. package/src/commands/project.test.ts +127 -0
  396. package/src/commands/project.ts +149 -0
  397. package/src/commands/sanitize.ts +60 -0
  398. package/src/commands/schema.ts +130 -0
  399. package/src/commands/setup.ts +509 -0
  400. package/src/commands/timeline.test.ts +499 -0
  401. package/src/commands/timeline.ts +529 -0
  402. package/src/commands/validate.ts +77 -0
  403. package/src/config/config.test.ts +197 -0
  404. package/src/config/index.ts +125 -0
  405. package/src/config/schema.ts +82 -0
  406. package/src/engine/index.ts +2 -0
  407. package/src/engine/project.test.ts +702 -0
  408. package/src/engine/project.ts +439 -0
  409. package/src/index.ts +146 -0
  410. package/src/utils/api-key.test.ts +41 -0
  411. package/src/utils/api-key.ts +247 -0
  412. package/src/utils/audio.ts +83 -0
  413. package/src/utils/exec-safe.ts +75 -0
  414. package/src/utils/first-run.ts +52 -0
  415. package/src/utils/provider-resolver.ts +56 -0
  416. package/src/utils/remotion.ts +951 -0
  417. package/src/utils/subtitle.test.ts +227 -0
  418. package/src/utils/subtitle.ts +169 -0
  419. package/src/utils/tty.ts +196 -0
  420. package/tsconfig.json +20 -0
@@ -0,0 +1,730 @@
1
+ import { Command } from "commander";
2
+ import { readFile, access, stat } from "node:fs/promises";
3
+ import { resolve, basename } from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import { Project } from "../engine/index.js";
8
+ import { execSafe, ffprobeDuration } from "../utils/exec-safe.js";
9
+ /**
10
+ * Resolve project file path - handles both file paths and directory paths
11
+ * If path is a directory, looks for project.vibe.json inside
12
+ */
13
+ async function resolveProjectPath(inputPath) {
14
+ const filePath = resolve(process.cwd(), inputPath);
15
+ try {
16
+ const stats = await stat(filePath);
17
+ if (stats.isDirectory()) {
18
+ return resolve(filePath, "project.vibe.json");
19
+ }
20
+ }
21
+ catch {
22
+ // Path doesn't exist or other error - let readFile handle it
23
+ }
24
+ return filePath;
25
+ }
26
+ /**
27
+ * Get the duration of a media file using ffprobe
28
+ * For images, returns a default duration since they have no inherent time
29
+ */
30
+ export async function getMediaDuration(filePath, mediaType, defaultImageDuration = 5) {
31
+ if (mediaType === "image") {
32
+ return defaultImageDuration;
33
+ }
34
+ try {
35
+ return await ffprobeDuration(filePath);
36
+ }
37
+ catch {
38
+ return defaultImageDuration;
39
+ }
40
+ }
41
+ /**
42
+ * Check if a media file has an audio stream
43
+ */
44
+ export async function checkHasAudio(filePath) {
45
+ try {
46
+ const { stdout } = await execSafe("ffprobe", [
47
+ "-v", "error", "-select_streams", "a", "-show_entries", "stream=codec_type", "-of", "default=noprint_wrappers=1:nokey=1", filePath,
48
+ ]);
49
+ return stdout.trim().length > 0;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ /**
56
+ * Reusable export function for programmatic usage
57
+ */
58
+ export async function runExport(projectPath, outputPath, options = {}) {
59
+ const { preset = "standard", format = "mp4", overwrite = false, gapFill = "extend" } = options;
60
+ try {
61
+ // Check if FFmpeg is installed
62
+ const ffmpegPath = await findFFmpeg();
63
+ if (!ffmpegPath) {
64
+ return {
65
+ success: false,
66
+ message: "FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)",
67
+ };
68
+ }
69
+ // Load project
70
+ const filePath = await resolveProjectPath(projectPath);
71
+ const content = await readFile(filePath, "utf-8");
72
+ const data = JSON.parse(content);
73
+ const project = Project.fromJSON(data);
74
+ const summary = project.getSummary();
75
+ if (summary.clipCount === 0) {
76
+ return {
77
+ success: false,
78
+ message: "Project has no clips to export",
79
+ };
80
+ }
81
+ // Determine output path
82
+ const finalOutputPath = resolve(process.cwd(), outputPath);
83
+ // Get preset settings
84
+ const presetSettings = getPresetSettings(preset, summary.aspectRatio);
85
+ // Get clips sorted by start time
86
+ const clips = project.getClips().sort((a, b) => a.startTime - b.startTime);
87
+ const sources = project.getSources();
88
+ // Verify source files exist and check for audio streams
89
+ const sourceAudioMap = new Map();
90
+ for (const clip of clips) {
91
+ const source = sources.find((s) => s.id === clip.sourceId);
92
+ if (source) {
93
+ try {
94
+ await access(source.url);
95
+ // Check if video source has audio
96
+ if (source.type === "video" && !sourceAudioMap.has(source.id)) {
97
+ sourceAudioMap.set(source.id, await checkHasAudio(source.url));
98
+ }
99
+ }
100
+ catch {
101
+ return {
102
+ success: false,
103
+ message: `Source file not found: ${source.url}`,
104
+ };
105
+ }
106
+ }
107
+ }
108
+ // Build FFmpeg command
109
+ const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, finalOutputPath, { overwrite, format, gapFill }, sourceAudioMap);
110
+ // Run FFmpeg
111
+ await runFFmpegProcess(ffmpegPath, ffmpegArgs, () => { });
112
+ return {
113
+ success: true,
114
+ message: `Exported: ${outputPath}`,
115
+ outputPath: finalOutputPath,
116
+ };
117
+ }
118
+ catch (error) {
119
+ const errorMessage = error instanceof Error ? error.message : String(error);
120
+ return {
121
+ success: false,
122
+ message: `Export failed: ${errorMessage}`,
123
+ };
124
+ }
125
+ }
126
+ export const exportCommand = new Command("export")
127
+ .description("Export project to video file")
128
+ .argument("<project>", "Project file path")
129
+ .option("-o, --output <path>", "Output file path")
130
+ .option("-f, --format <format>", "Output format (mp4, webm, mov)", "mp4")
131
+ .option("-p, --preset <preset>", "Quality preset (draft, standard, high, ultra)", "standard")
132
+ .option("-y, --overwrite", "Overwrite output file if exists", false)
133
+ .option("-g, --gap-fill <strategy>", "Gap filling strategy (black, extend)", "extend")
134
+ .action(async (projectPath, options) => {
135
+ const spinner = ora("Checking FFmpeg...").start();
136
+ try {
137
+ // Check if FFmpeg is installed
138
+ const ffmpegPath = await findFFmpeg();
139
+ if (!ffmpegPath) {
140
+ spinner.fail(chalk.red("FFmpeg not found"));
141
+ console.error();
142
+ console.error(chalk.yellow("Please install FFmpeg:"));
143
+ console.error(chalk.dim(" macOS: brew install ffmpeg"));
144
+ console.error(chalk.dim(" Ubuntu: sudo apt install ffmpeg"));
145
+ console.error(chalk.dim(" Windows: winget install ffmpeg"));
146
+ process.exit(1);
147
+ }
148
+ // Load project
149
+ spinner.text = "Loading project...";
150
+ const filePath = await resolveProjectPath(projectPath);
151
+ const content = await readFile(filePath, "utf-8");
152
+ const data = JSON.parse(content);
153
+ const project = Project.fromJSON(data);
154
+ const summary = project.getSummary();
155
+ if (summary.clipCount === 0) {
156
+ spinner.fail(chalk.red("Project has no clips to export"));
157
+ process.exit(1);
158
+ }
159
+ // Determine output path
160
+ const outputPath = options.output
161
+ ? resolve(process.cwd(), options.output)
162
+ : resolve(process.cwd(), `${basename(projectPath, ".vibe.json")}.${options.format}`);
163
+ // Get preset settings
164
+ const presetSettings = getPresetSettings(options.preset, summary.aspectRatio);
165
+ // Get clips sorted by start time
166
+ const clips = project.getClips().sort((a, b) => a.startTime - b.startTime);
167
+ const sources = project.getSources();
168
+ // Verify source files exist and check for audio streams
169
+ spinner.text = "Verifying source files...";
170
+ const sourceAudioMap = new Map();
171
+ for (const clip of clips) {
172
+ const source = sources.find((s) => s.id === clip.sourceId);
173
+ if (source) {
174
+ try {
175
+ await access(source.url);
176
+ // Check if video source has audio
177
+ if (source.type === "video" && !sourceAudioMap.has(source.id)) {
178
+ sourceAudioMap.set(source.id, await checkHasAudio(source.url));
179
+ }
180
+ }
181
+ catch {
182
+ spinner.fail(chalk.red(`Source file not found: ${source.url}`));
183
+ process.exit(1);
184
+ }
185
+ }
186
+ }
187
+ // Build FFmpeg command
188
+ spinner.text = "Building export command...";
189
+ const gapFillStrategy = (options.gapFill === "black" ? "black" : "extend");
190
+ const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, outputPath, { ...options, gapFill: gapFillStrategy }, sourceAudioMap);
191
+ if (process.env.DEBUG) {
192
+ console.log("\nFFmpeg command:");
193
+ console.log("ffmpeg", ffmpegArgs.join(" "));
194
+ console.log();
195
+ }
196
+ // Run FFmpeg
197
+ spinner.text = "Encoding...";
198
+ await runFFmpegProcess(ffmpegPath, ffmpegArgs, (progress) => {
199
+ spinner.text = `Encoding... ${progress}%`;
200
+ });
201
+ spinner.succeed(chalk.green(`Exported: ${outputPath}`));
202
+ console.log();
203
+ console.log(chalk.dim(" Duration:"), `${summary.duration.toFixed(1)}s`);
204
+ console.log(chalk.dim(" Clips:"), summary.clipCount);
205
+ console.log(chalk.dim(" Format:"), options.format);
206
+ console.log(chalk.dim(" Preset:"), options.preset);
207
+ console.log(chalk.dim(" Resolution:"), presetSettings.resolution);
208
+ console.log();
209
+ }
210
+ catch (error) {
211
+ spinner.fail(chalk.red("Export failed"));
212
+ if (error instanceof Error) {
213
+ console.error(chalk.red(error.message));
214
+ if (process.env.DEBUG) {
215
+ console.error(error.stack);
216
+ }
217
+ }
218
+ process.exit(1);
219
+ }
220
+ });
221
+ /**
222
+ * Find FFmpeg executable
223
+ */
224
+ async function findFFmpeg() {
225
+ try {
226
+ const { stdout } = await execSafe("which", ["ffmpeg"]);
227
+ return stdout.trim().split("\n")[0];
228
+ }
229
+ catch {
230
+ try {
231
+ const { stdout } = await execSafe("where", ["ffmpeg"]);
232
+ return stdout.trim().split("\n")[0];
233
+ }
234
+ catch {
235
+ return null;
236
+ }
237
+ }
238
+ }
239
+ /**
240
+ * Detect gaps in timeline between clips
241
+ * Returns array of gaps with start and end times
242
+ */
243
+ function detectTimelineGaps(clips, totalDuration) {
244
+ if (clips.length === 0)
245
+ return [];
246
+ const gaps = [];
247
+ const sortedClips = [...clips].sort((a, b) => a.startTime - b.startTime);
248
+ // Check for gap at the start (first clip doesn't start at 0)
249
+ if (sortedClips[0].startTime > 0.001) {
250
+ gaps.push({ start: 0, end: sortedClips[0].startTime });
251
+ }
252
+ // Check for gaps between clips
253
+ for (let i = 0; i < sortedClips.length - 1; i++) {
254
+ const clipEnd = sortedClips[i].startTime + sortedClips[i].duration;
255
+ const nextStart = sortedClips[i + 1].startTime;
256
+ // Allow small tolerance for floating point errors
257
+ if (nextStart > clipEnd + 0.001) {
258
+ gaps.push({ start: clipEnd, end: nextStart });
259
+ }
260
+ }
261
+ // Check for gap at the end if totalDuration is provided
262
+ if (totalDuration !== undefined) {
263
+ const lastClip = sortedClips[sortedClips.length - 1];
264
+ const lastClipEnd = lastClip.startTime + lastClip.duration;
265
+ if (totalDuration > lastClipEnd + 0.001) {
266
+ gaps.push({ start: lastClipEnd, end: totalDuration });
267
+ }
268
+ }
269
+ return gaps;
270
+ }
271
+ /**
272
+ * Create gap fill plans by extending adjacent clips
273
+ * Priority:
274
+ * 1. Extend clip AFTER the gap backwards (if sourceStartOffset > 0)
275
+ * 2. Extend clip BEFORE the gap forwards (if source has unused duration)
276
+ * 3. Fallback to black frames
277
+ */
278
+ function createGapFillPlans(gaps, clips, sources) {
279
+ const sortedClips = [...clips].sort((a, b) => a.startTime - b.startTime);
280
+ return gaps.map((gap) => {
281
+ const fills = [];
282
+ let remainingStart = gap.start;
283
+ let remainingEnd = gap.end;
284
+ // Find clip AFTER the gap (for extending backwards)
285
+ const clipAfter = sortedClips.find((c) => Math.abs(c.startTime - gap.end) < 0.01);
286
+ // Find clip BEFORE the gap (for extending forwards)
287
+ const clipBefore = sortedClips.find((c) => Math.abs((c.startTime + c.duration) - gap.start) < 0.01);
288
+ // Try extending clip after the gap backwards first
289
+ if (clipAfter && clipAfter.sourceStartOffset > 0.01) {
290
+ const source = sources.find((s) => s.id === clipAfter.sourceId);
291
+ if (source && source.type === "video") {
292
+ const availableExtension = clipAfter.sourceStartOffset;
293
+ const extensionDuration = Math.min(availableExtension, remainingEnd - remainingStart);
294
+ if (extensionDuration > 0.01) {
295
+ // Extend from the gap end backwards
296
+ const fillStart = remainingEnd - extensionDuration;
297
+ const sourceStart = clipAfter.sourceStartOffset - extensionDuration;
298
+ const sourceEnd = clipAfter.sourceStartOffset;
299
+ fills.push({
300
+ type: "extend-after",
301
+ sourceId: source.id,
302
+ sourceUrl: source.url,
303
+ start: fillStart,
304
+ end: remainingEnd,
305
+ sourceStart,
306
+ sourceEnd,
307
+ });
308
+ remainingEnd = fillStart;
309
+ }
310
+ }
311
+ }
312
+ // If there's still a gap, try extending clip before the gap forwards
313
+ if (remainingEnd - remainingStart > 0.01 && clipBefore) {
314
+ const source = sources.find((s) => s.id === clipBefore.sourceId);
315
+ if (source && source.type === "video") {
316
+ const usedEndInSource = clipBefore.sourceEndOffset;
317
+ const availableExtension = source.duration - usedEndInSource;
318
+ if (availableExtension > 0.01) {
319
+ const extensionDuration = Math.min(availableExtension, remainingEnd - remainingStart);
320
+ if (extensionDuration > 0.01) {
321
+ const sourceStart = usedEndInSource;
322
+ const sourceEnd = usedEndInSource + extensionDuration;
323
+ fills.push({
324
+ type: "extend-before",
325
+ sourceId: source.id,
326
+ sourceUrl: source.url,
327
+ start: remainingStart,
328
+ end: remainingStart + extensionDuration,
329
+ sourceStart,
330
+ sourceEnd,
331
+ });
332
+ remainingStart = remainingStart + extensionDuration;
333
+ }
334
+ }
335
+ }
336
+ }
337
+ // Fill any remaining gap with black
338
+ if (remainingEnd - remainingStart > 0.01) {
339
+ fills.push({
340
+ type: "black",
341
+ start: remainingStart,
342
+ end: remainingEnd,
343
+ });
344
+ }
345
+ return { gap, fills };
346
+ });
347
+ }
348
+ /**
349
+ * Build FFmpeg arguments for export
350
+ */
351
+ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, sourceAudioMap = new Map()) {
352
+ const args = [];
353
+ // Overwrite flag first
354
+ if (options.overwrite) {
355
+ args.push("-y");
356
+ }
357
+ // Add input files
358
+ const sourceMap = new Map();
359
+ let inputIndex = 0;
360
+ for (const clip of clips) {
361
+ const source = sources.find((s) => s.id === clip.sourceId);
362
+ if (source && !sourceMap.has(source.id)) {
363
+ // Add -loop 1 before image inputs to create a continuous video stream
364
+ if (source.type === "image") {
365
+ args.push("-loop", "1");
366
+ }
367
+ args.push("-i", source.url);
368
+ sourceMap.set(source.id, inputIndex);
369
+ inputIndex++;
370
+ }
371
+ }
372
+ // Build filter complex
373
+ const filterParts = [];
374
+ // Separate clips by track type for proper timeline-based export
375
+ // Get track info to determine clip types
376
+ const videoClips = clips.filter((clip) => {
377
+ const source = sources.find((s) => s.id === clip.sourceId);
378
+ return source && (source.type === "image" || source.type === "video");
379
+ }).sort((a, b) => a.startTime - b.startTime);
380
+ // Include audio clips from:
381
+ // 1. Explicit audio sources (narration, music)
382
+ // 2. Video sources when there are NO separate audio clips (e.g., highlight reels)
383
+ const explicitAudioClips = clips.filter((clip) => {
384
+ const source = sources.find((s) => s.id === clip.sourceId);
385
+ return source && source.type === "audio";
386
+ }).sort((a, b) => a.startTime - b.startTime);
387
+ // If no explicit audio clips, extract audio from video clips
388
+ const audioClips = explicitAudioClips.length > 0
389
+ ? explicitAudioClips
390
+ : clips.filter((clip) => {
391
+ const source = sources.find((s) => s.id === clip.sourceId);
392
+ return source && source.type === "video";
393
+ }).sort((a, b) => a.startTime - b.startTime);
394
+ // Get target resolution for scaling (all clips must match for concat)
395
+ const [targetWidth, targetHeight] = presetSettings.resolution.split("x").map(Number);
396
+ // Detect gaps in video timeline
397
+ // For totalDuration, use the longest audio clip end time if explicit audio exists
398
+ // (audio is usually the reference for timing in b-roll scenarios)
399
+ let totalDuration;
400
+ if (explicitAudioClips.length > 0) {
401
+ const audioEnd = Math.max(...explicitAudioClips.map(c => c.startTime + c.duration));
402
+ totalDuration = audioEnd;
403
+ }
404
+ const videoGaps = detectTimelineGaps(videoClips, totalDuration);
405
+ // Create gap fill plans based on strategy
406
+ const gapFillStrategy = options.gapFill || "extend";
407
+ const gapFillPlans = gapFillStrategy === "extend"
408
+ ? createGapFillPlans(videoGaps, videoClips, sources)
409
+ : videoGaps.map((gap) => ({
410
+ gap,
411
+ fills: [{ type: "black", start: gap.start, end: gap.end }],
412
+ }));
413
+ const videoSegments = [];
414
+ // Add video clips as segments
415
+ for (const clip of videoClips) {
416
+ videoSegments.push({ type: 'clip', clip, startTime: clip.startTime });
417
+ }
418
+ // Add gap fills as segments (from gap fill plans)
419
+ for (const plan of gapFillPlans) {
420
+ for (const fill of plan.fills) {
421
+ if (fill.type === "black") {
422
+ videoSegments.push({
423
+ type: 'black',
424
+ startTime: fill.start,
425
+ duration: fill.end - fill.start,
426
+ });
427
+ }
428
+ else {
429
+ // extend-before or extend-after
430
+ videoSegments.push({
431
+ type: 'extended',
432
+ sourceId: fill.sourceId,
433
+ sourceUrl: fill.sourceUrl,
434
+ startTime: fill.start,
435
+ duration: fill.end - fill.start,
436
+ sourceStart: fill.sourceStart,
437
+ sourceEnd: fill.sourceEnd,
438
+ });
439
+ }
440
+ }
441
+ }
442
+ // Sort by start time
443
+ videoSegments.sort((a, b) => a.startTime - b.startTime);
444
+ // Process video segments (clips, extended clips, and black frames)
445
+ const videoStreams = [];
446
+ let videoStreamIdx = 0;
447
+ for (const segment of videoSegments) {
448
+ if (segment.type === 'clip' && segment.clip) {
449
+ const clip = segment.clip;
450
+ const source = sources.find((s) => s.id === clip.sourceId);
451
+ if (!source)
452
+ continue;
453
+ const srcIdx = sourceMap.get(source.id);
454
+ if (srcIdx === undefined)
455
+ continue;
456
+ // Video filter chain - images need different handling than video
457
+ let videoFilter;
458
+ if (source.type === "image") {
459
+ // Images: trim from 0 to clip duration (no source offset since images are looped)
460
+ videoFilter = `[${srcIdx}:v]trim=start=0:end=${clip.duration},setpts=PTS-STARTPTS`;
461
+ }
462
+ else {
463
+ // Video: use source offsets
464
+ const trimStart = clip.sourceStartOffset;
465
+ const trimEnd = clip.sourceStartOffset + clip.duration;
466
+ videoFilter = `[${srcIdx}:v]trim=start=${trimStart}:end=${trimEnd},setpts=PTS-STARTPTS`;
467
+ }
468
+ // Scale to target resolution for concat compatibility (force same size, pad if needed)
469
+ videoFilter += `,scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
470
+ // Apply effects
471
+ for (const effect of clip.effects || []) {
472
+ if (effect.type === "fadeIn") {
473
+ videoFilter += `,fade=t=in:st=0:d=${effect.duration}`;
474
+ }
475
+ else if (effect.type === "fadeOut") {
476
+ const fadeStart = clip.duration - effect.duration;
477
+ videoFilter += `,fade=t=out:st=${fadeStart}:d=${effect.duration}`;
478
+ }
479
+ }
480
+ videoFilter += `[v${videoStreamIdx}]`;
481
+ filterParts.push(videoFilter);
482
+ videoStreams.push(`[v${videoStreamIdx}]`);
483
+ videoStreamIdx++;
484
+ }
485
+ else if (segment.type === 'extended' && segment.sourceId) {
486
+ // Extended segment - use source video to fill gap
487
+ const srcIdx = sourceMap.get(segment.sourceId);
488
+ if (srcIdx === undefined) {
489
+ // Fallback to black if source not found in input map
490
+ const gapFilter = `color=c=black:s=${targetWidth}x${targetHeight}:d=${segment.duration}:r=30,format=yuv420p[v${videoStreamIdx}]`;
491
+ filterParts.push(gapFilter);
492
+ videoStreams.push(`[v${videoStreamIdx}]`);
493
+ videoStreamIdx++;
494
+ continue;
495
+ }
496
+ const videoFilter = `[${srcIdx}:v]trim=start=${segment.sourceStart}:end=${segment.sourceEnd},setpts=PTS-STARTPTS,scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:(ow-iw)/2:(oh-ih)/2,setsar=1[v${videoStreamIdx}]`;
497
+ filterParts.push(videoFilter);
498
+ videoStreams.push(`[v${videoStreamIdx}]`);
499
+ videoStreamIdx++;
500
+ }
501
+ else if (segment.type === 'black') {
502
+ // Generate black frame for the gap duration
503
+ const gapFilter = `color=c=black:s=${targetWidth}x${targetHeight}:d=${segment.duration}:r=30,format=yuv420p[v${videoStreamIdx}]`;
504
+ filterParts.push(gapFilter);
505
+ videoStreams.push(`[v${videoStreamIdx}]`);
506
+ videoStreamIdx++;
507
+ }
508
+ }
509
+ // Detect gaps in audio timeline (use same totalDuration for consistency)
510
+ const audioGaps = detectTimelineGaps(audioClips, totalDuration);
511
+ const audioSegments = [];
512
+ // Add audio clips as segments
513
+ for (const clip of audioClips) {
514
+ audioSegments.push({ type: 'clip', clip, startTime: clip.startTime });
515
+ }
516
+ // Add gaps as segments
517
+ for (const gap of audioGaps) {
518
+ audioSegments.push({ type: 'gap', gap, startTime: gap.start });
519
+ }
520
+ // Sort by start time
521
+ audioSegments.sort((a, b) => a.startTime - b.startTime);
522
+ // Process audio segments (clips and gaps)
523
+ const audioStreams = [];
524
+ let audioStreamIdx = 0;
525
+ for (const segment of audioSegments) {
526
+ if (segment.type === 'clip' && segment.clip) {
527
+ const clip = segment.clip;
528
+ const source = sources.find((s) => s.id === clip.sourceId);
529
+ if (!source)
530
+ continue;
531
+ const srcIdx = sourceMap.get(source.id);
532
+ if (srcIdx === undefined)
533
+ continue;
534
+ // Check if source has audio (audio sources always have audio, video sources need to be checked)
535
+ const hasAudio = source.type === "audio" || sourceAudioMap.get(source.id) === true;
536
+ let audioFilter;
537
+ if (hasAudio) {
538
+ const audioTrimStart = clip.sourceStartOffset;
539
+ const audioTrimEnd = clip.sourceStartOffset + clip.duration;
540
+ const sourceDuration = source.duration || 0;
541
+ const clipDuration = clip.duration;
542
+ if (source.type === "audio" && sourceDuration > clipDuration && audioTrimStart === 0) {
543
+ // Audio source is longer than clip slot — speed up to fit instead of truncating
544
+ const tempo = sourceDuration / clipDuration;
545
+ if (tempo <= 2.0) {
546
+ // atempo sounds natural up to ~1.3x, acceptable up to 2x
547
+ audioFilter = `[${srcIdx}:a]atempo=${tempo.toFixed(4)},asetpts=PTS-STARTPTS`;
548
+ }
549
+ else {
550
+ // Too fast would sound bad — fall back to trim
551
+ audioFilter = `[${srcIdx}:a]atrim=start=${audioTrimStart}:end=${audioTrimEnd},asetpts=PTS-STARTPTS`;
552
+ }
553
+ }
554
+ else {
555
+ // Normal trim for video-embedded audio, audio that fits, or offset clips
556
+ audioFilter = `[${srcIdx}:a]atrim=start=${audioTrimStart}:end=${audioTrimEnd},asetpts=PTS-STARTPTS`;
557
+ }
558
+ }
559
+ else {
560
+ // Source has no audio - generate silence for the clip duration
561
+ audioFilter = `anullsrc=r=48000:cl=stereo,atrim=0:${clip.duration},asetpts=PTS-STARTPTS`;
562
+ }
563
+ // Apply audio effects
564
+ for (const effect of clip.effects || []) {
565
+ if (effect.type === "fadeIn") {
566
+ audioFilter += `,afade=t=in:st=0:d=${effect.duration}`;
567
+ }
568
+ else if (effect.type === "fadeOut") {
569
+ const fadeStart = clip.duration - effect.duration;
570
+ audioFilter += `,afade=t=out:st=${fadeStart}:d=${effect.duration}`;
571
+ }
572
+ }
573
+ audioFilter += `[a${audioStreamIdx}]`;
574
+ filterParts.push(audioFilter);
575
+ audioStreams.push(`[a${audioStreamIdx}]`);
576
+ audioStreamIdx++;
577
+ }
578
+ else if (segment.type === 'gap' && segment.gap) {
579
+ // Generate silence for the gap duration
580
+ const gapDuration = segment.gap.end - segment.gap.start;
581
+ const audioGapFilter = `anullsrc=r=48000:cl=stereo,atrim=0:${gapDuration},asetpts=PTS-STARTPTS[a${audioStreamIdx}]`;
582
+ filterParts.push(audioGapFilter);
583
+ audioStreams.push(`[a${audioStreamIdx}]`);
584
+ audioStreamIdx++;
585
+ }
586
+ }
587
+ // Concatenate video clips
588
+ if (videoStreams.length > 1) {
589
+ filterParts.push(`${videoStreams.join("")}concat=n=${videoStreams.length}:v=1:a=0[outv]`);
590
+ }
591
+ else if (videoStreams.length === 1) {
592
+ // Single video clip - just copy
593
+ filterParts.push(`${videoStreams[0]}copy[outv]`);
594
+ }
595
+ // Concatenate or mix audio clips
596
+ if (audioStreams.length > 1) {
597
+ filterParts.push(`${audioStreams.join("")}concat=n=${audioStreams.length}:v=0:a=1[outa]`);
598
+ }
599
+ else if (audioStreams.length === 1) {
600
+ // Single audio clip - just copy
601
+ filterParts.push(`${audioStreams[0]}acopy[outa]`);
602
+ }
603
+ // Add filter complex
604
+ args.push("-filter_complex", filterParts.join(";"));
605
+ // Map outputs
606
+ args.push("-map", "[outv]");
607
+ if (audioStreams.length > 0) {
608
+ args.push("-map", "[outa]");
609
+ }
610
+ // Add encoding settings
611
+ args.push(...presetSettings.ffmpegArgs);
612
+ // Output file
613
+ args.push(outputPath);
614
+ return args;
615
+ }
616
+ /**
617
+ * Run FFmpeg with progress reporting
618
+ */
619
+ function runFFmpegProcess(ffmpegPath, args, onProgress) {
620
+ return new Promise((resolve, reject) => {
621
+ const ffmpeg = spawn(ffmpegPath, args, {
622
+ stdio: ["pipe", "pipe", "pipe"],
623
+ });
624
+ let duration = 0;
625
+ let stderr = "";
626
+ ffmpeg.stderr?.on("data", (data) => {
627
+ const output = data.toString();
628
+ stderr += output;
629
+ // Parse duration
630
+ const durationMatch = output.match(/Duration: (\d+):(\d+):(\d+\.\d+)/);
631
+ if (durationMatch) {
632
+ const [, hours, minutes, seconds] = durationMatch;
633
+ duration =
634
+ parseInt(hours) * 3600 +
635
+ parseInt(minutes) * 60 +
636
+ parseFloat(seconds);
637
+ }
638
+ // Parse progress
639
+ const timeMatch = output.match(/time=(\d+):(\d+):(\d+\.\d+)/);
640
+ if (timeMatch && duration > 0) {
641
+ const [, hours, minutes, seconds] = timeMatch;
642
+ const currentTime = parseInt(hours) * 3600 +
643
+ parseInt(minutes) * 60 +
644
+ parseFloat(seconds);
645
+ const percent = Math.min(100, Math.round((currentTime / duration) * 100));
646
+ onProgress(percent);
647
+ }
648
+ });
649
+ ffmpeg.on("close", (code) => {
650
+ if (code === 0) {
651
+ resolve();
652
+ }
653
+ else {
654
+ // Extract error message
655
+ const errorMatch = stderr.match(/Error.*$/m);
656
+ const errorMsg = errorMatch ? errorMatch[0] : `FFmpeg exited with code ${code}`;
657
+ reject(new Error(errorMsg));
658
+ }
659
+ });
660
+ ffmpeg.on("error", (err) => {
661
+ reject(err);
662
+ });
663
+ });
664
+ }
665
+ function getPresetSettings(preset, aspectRatio) {
666
+ const presets = {
667
+ draft: {
668
+ resolution: "640x360",
669
+ videoBitrate: "1M",
670
+ audioBitrate: "128k",
671
+ ffmpegArgs: [
672
+ "-c:v", "libx264",
673
+ "-preset", "ultrafast",
674
+ "-crf", "28",
675
+ "-c:a", "aac",
676
+ "-b:a", "128k",
677
+ ],
678
+ },
679
+ standard: {
680
+ resolution: "1280x720",
681
+ videoBitrate: "4M",
682
+ audioBitrate: "192k",
683
+ ffmpegArgs: [
684
+ "-c:v", "libx264",
685
+ "-preset", "medium",
686
+ "-crf", "23",
687
+ "-c:a", "aac",
688
+ "-b:a", "192k",
689
+ ],
690
+ },
691
+ high: {
692
+ resolution: "1920x1080",
693
+ videoBitrate: "8M",
694
+ audioBitrate: "256k",
695
+ ffmpegArgs: [
696
+ "-c:v", "libx264",
697
+ "-preset", "slow",
698
+ "-crf", "18",
699
+ "-c:a", "aac",
700
+ "-b:a", "256k",
701
+ ],
702
+ },
703
+ ultra: {
704
+ resolution: "3840x2160",
705
+ videoBitrate: "20M",
706
+ audioBitrate: "320k",
707
+ ffmpegArgs: [
708
+ "-c:v", "libx264",
709
+ "-preset", "slow",
710
+ "-crf", "15",
711
+ "-c:a", "aac",
712
+ "-b:a", "320k",
713
+ ],
714
+ },
715
+ };
716
+ // Adjust resolution for aspect ratio
717
+ const settings = { ...presets[preset] || presets.standard };
718
+ if (aspectRatio === "9:16") {
719
+ // Vertical video
720
+ const [w, h] = settings.resolution.split("x");
721
+ settings.resolution = `${h}x${w}`;
722
+ }
723
+ else if (aspectRatio === "1:1") {
724
+ // Square video
725
+ const h = settings.resolution.split("x")[1];
726
+ settings.resolution = `${h}x${h}`;
727
+ }
728
+ return settings;
729
+ }
730
+ //# sourceMappingURL=export.js.map