voxflow 1.14.0 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (454) hide show
  1. package/README.md +69 -2
  2. package/bin/voxflow.js +27 -0
  3. package/dist/index.js +1 -1
  4. package/dist/remotion-bundle/02a2fb2eb80bc7bf.woff2 +0 -0
  5. package/dist/remotion-bundle/052ca5351e5e06ba.woff2 +0 -0
  6. package/dist/remotion-bundle/05853dd28f4019cb.woff2 +0 -0
  7. package/dist/remotion-bundle/072ead3737f7c0d0.woff2 +0 -0
  8. package/dist/remotion-bundle/07d4248613c86a2e.woff2 +0 -0
  9. package/dist/remotion-bundle/0884a5c2d1d2d99b.woff2 +0 -0
  10. package/dist/remotion-bundle/0b0e185b2752095e.woff2 +0 -0
  11. package/dist/remotion-bundle/0e66c11bde067d91.woff2 +0 -0
  12. package/dist/remotion-bundle/0f7794cfba2c5d21.woff2 +0 -0
  13. package/dist/remotion-bundle/0fdbae5a4365783a.woff2 +0 -0
  14. package/dist/remotion-bundle/112.bundle.js +11 -0
  15. package/dist/remotion-bundle/112.bundle.js.map +1 -0
  16. package/dist/remotion-bundle/113.bundle.js +11 -0
  17. package/dist/remotion-bundle/113.bundle.js.map +1 -0
  18. package/dist/remotion-bundle/119cae0c4c16f7ed.woff2 +0 -0
  19. package/dist/remotion-bundle/14725f649fd1e78c.woff2 +0 -0
  20. package/dist/remotion-bundle/14abe9e3f95f7888.woff2 +0 -0
  21. package/dist/remotion-bundle/163.bundle.js +14678 -0
  22. package/dist/remotion-bundle/163.bundle.js.map +1 -0
  23. package/dist/remotion-bundle/1808c54072bf6d14.woff2 +0 -0
  24. package/dist/remotion-bundle/18948bec3e3012fe.woff2 +0 -0
  25. package/dist/remotion-bundle/1a661c60d0fc84fc.woff2 +0 -0
  26. package/dist/remotion-bundle/1af94941e1bc7e1e.woff2 +0 -0
  27. package/dist/remotion-bundle/1bee0219595f606c.woff2 +0 -0
  28. package/dist/remotion-bundle/1bfd5da7ce9d4ec4.woff2 +0 -0
  29. package/dist/remotion-bundle/1c158d56f1884f3f.woff2 +0 -0
  30. package/dist/remotion-bundle/1cf5e88e667610eb.woff2 +0 -0
  31. package/dist/remotion-bundle/1d431bd10f53c481.woff2 +0 -0
  32. package/dist/remotion-bundle/1d701a81a7670db2.woff2 +0 -0
  33. package/dist/remotion-bundle/1da0fecad4240f16.woff2 +0 -0
  34. package/dist/remotion-bundle/1ed14d3d0c5c63fe.woff2 +0 -0
  35. package/dist/remotion-bundle/1edfecf40e586f53.woff2 +0 -0
  36. package/dist/remotion-bundle/1f479711bc34b054.woff +0 -0
  37. package/dist/remotion-bundle/1f86e54a0ff5fcd1.woff2 +0 -0
  38. package/dist/remotion-bundle/2043ea87d9aabd11.woff2 +0 -0
  39. package/dist/remotion-bundle/20563c39ee8a0e40.woff2 +0 -0
  40. package/dist/remotion-bundle/20c231590fd12c44.woff2 +0 -0
  41. package/dist/remotion-bundle/20ce61713f754c07.woff2 +0 -0
  42. package/dist/remotion-bundle/21eb9306fce24bb1.woff2 +0 -0
  43. package/dist/remotion-bundle/244bf71c0cc851af.woff2 +0 -0
  44. package/dist/remotion-bundle/274d4cfc02bffbcb.woff2 +0 -0
  45. package/dist/remotion-bundle/275.bundle.js +21 -0
  46. package/dist/remotion-bundle/275.bundle.js.map +1 -0
  47. package/dist/remotion-bundle/2958f540b39513dc.woff2 +0 -0
  48. package/dist/remotion-bundle/2a168b98fd97722e.woff2 +0 -0
  49. package/dist/remotion-bundle/2d1f6373937ab55f.woff2 +0 -0
  50. package/dist/remotion-bundle/2d213ae47ff6daa9.woff2 +0 -0
  51. package/dist/remotion-bundle/2e4b1f04fcd05047.woff2 +0 -0
  52. package/dist/remotion-bundle/304170d98f4c4563.woff2 +0 -0
  53. package/dist/remotion-bundle/30d02e136e7a5642.woff2 +0 -0
  54. package/dist/remotion-bundle/3135562b52a714cd.woff2 +0 -0
  55. package/dist/remotion-bundle/313713af2c8144e9.woff2 +0 -0
  56. package/dist/remotion-bundle/325fa4108d2285b9.woff2 +0 -0
  57. package/dist/remotion-bundle/338e927ed3345e0c.woff2 +0 -0
  58. package/dist/remotion-bundle/35fc6b190365bc17.woff2 +0 -0
  59. package/dist/remotion-bundle/37a51f1122d4efc5.woff2 +0 -0
  60. package/dist/remotion-bundle/39a4d63e02736f5e.woff2 +0 -0
  61. package/dist/remotion-bundle/3a00e0d62dfc4171.woff2 +0 -0
  62. package/dist/remotion-bundle/3a6955e6561affe1.woff2 +0 -0
  63. package/dist/remotion-bundle/3c573945aef49b89.woff2 +0 -0
  64. package/dist/remotion-bundle/3cdbfbfa23b516a5.woff2 +0 -0
  65. package/dist/remotion-bundle/3e42f85a9e64ca8a.woff2 +0 -0
  66. package/dist/remotion-bundle/3e83eaf1ec859415.woff2 +0 -0
  67. package/dist/remotion-bundle/3f3c8c90de1250ee.woff2 +0 -0
  68. package/dist/remotion-bundle/434.bundle.js +205 -0
  69. package/dist/remotion-bundle/434.bundle.js.map +1 -0
  70. package/dist/remotion-bundle/44ffc6ca4d781692.woff2 +0 -0
  71. package/dist/remotion-bundle/4670d9c4580b09eb.woff2 +0 -0
  72. package/dist/remotion-bundle/479756881b302824.woff2 +0 -0
  73. package/dist/remotion-bundle/481b82134bfa9c82.woff2 +0 -0
  74. package/dist/remotion-bundle/48d27029626f4328.woff2 +0 -0
  75. package/dist/remotion-bundle/49b7b2a30329c511.woff2 +0 -0
  76. package/dist/remotion-bundle/4c8b25a1a9337045.woff2 +0 -0
  77. package/dist/remotion-bundle/4cba14788ca9259b.woff2 +0 -0
  78. package/dist/remotion-bundle/4cd6c589c004a6a7.woff2 +0 -0
  79. package/dist/remotion-bundle/4cd8d79c1021608d.woff2 +0 -0
  80. package/dist/remotion-bundle/4d8fa99b3f00f9f0.woff2 +0 -0
  81. package/dist/remotion-bundle/4e7805a643f86d53.woff2 +0 -0
  82. package/dist/remotion-bundle/4ff91be454542e3f.woff2 +0 -0
  83. package/dist/remotion-bundle/504cbcba1f63591b.woff2 +0 -0
  84. package/dist/remotion-bundle/5202d792e5791d6c.woff2 +0 -0
  85. package/dist/remotion-bundle/534db5ad4770cc1d.woff2 +0 -0
  86. package/dist/remotion-bundle/53b9568eb85f866b.woff2 +0 -0
  87. package/dist/remotion-bundle/543ad386ca171de9.woff2 +0 -0
  88. package/dist/remotion-bundle/54798e55bbf7976e.woff2 +0 -0
  89. package/dist/remotion-bundle/580.bundle.js +11 -0
  90. package/dist/remotion-bundle/580.bundle.js.map +1 -0
  91. package/dist/remotion-bundle/58d174d1193af6d1.woff2 +0 -0
  92. package/dist/remotion-bundle/591d29ff3ff53c80.woff2 +0 -0
  93. package/dist/remotion-bundle/5c28c4f4824383c6.woff2 +0 -0
  94. package/dist/remotion-bundle/5da9740d2ce894c8.woff2 +0 -0
  95. package/dist/remotion-bundle/6197735364642360.woff2 +0 -0
  96. package/dist/remotion-bundle/6265a4335724080f.woff2 +0 -0
  97. package/dist/remotion-bundle/633f5e4f6394daa7.woff2 +0 -0
  98. package/dist/remotion-bundle/637d95ace6a69c49.woff2 +0 -0
  99. package/dist/remotion-bundle/648e04a04dacff8f.woff2 +0 -0
  100. package/dist/remotion-bundle/64a6e83045a008b2.woff2 +0 -0
  101. package/dist/remotion-bundle/651.bundle.js +11 -0
  102. package/dist/remotion-bundle/651.bundle.js.map +1 -0
  103. package/dist/remotion-bundle/65e2a988c070facc.woff2 +0 -0
  104. package/dist/remotion-bundle/66a2f6ce5cc69105.woff2 +0 -0
  105. package/dist/remotion-bundle/690.bundle.js +3479 -0
  106. package/dist/remotion-bundle/690.bundle.js.map +1 -0
  107. package/dist/remotion-bundle/690ff55252ca715d.woff2 +0 -0
  108. package/dist/remotion-bundle/6a01a1cff49314fc.woff2 +0 -0
  109. package/dist/remotion-bundle/6cbc32670982986c.woff2 +0 -0
  110. package/dist/remotion-bundle/6d3cc42ae547f454.woff2 +0 -0
  111. package/dist/remotion-bundle/6d8f4cfa1ddc0830.woff2 +0 -0
  112. package/dist/remotion-bundle/6e4d7c6ae65e2dc3.woff2 +0 -0
  113. package/dist/remotion-bundle/6e86418bbcefb2e8.woff2 +0 -0
  114. package/dist/remotion-bundle/6ee02884b29cf7fb.woff2 +0 -0
  115. package/dist/remotion-bundle/6f436a74c9e3252c.woff2 +0 -0
  116. package/dist/remotion-bundle/78c8022f1657618b.woff2 +0 -0
  117. package/dist/remotion-bundle/7c5444169792bca4.woff2 +0 -0
  118. package/dist/remotion-bundle/7c86bddd9d997212.woff2 +0 -0
  119. package/dist/remotion-bundle/7e1284684767f584.woff2 +0 -0
  120. package/dist/remotion-bundle/7e81c17522d182b2.woff2 +0 -0
  121. package/dist/remotion-bundle/7eb87be198f7858c.woff2 +0 -0
  122. package/dist/remotion-bundle/8060c928f948aab5.woff2 +0 -0
  123. package/dist/remotion-bundle/80bc9dfbea2b35ae.woff2 +0 -0
  124. package/dist/remotion-bundle/811b83f69963bb48.woff2 +0 -0
  125. package/dist/remotion-bundle/813.bundle.js +117511 -0
  126. package/dist/remotion-bundle/813.bundle.js.map +1 -0
  127. package/dist/remotion-bundle/84df492e349f82e9.woff2 +0 -0
  128. package/dist/remotion-bundle/8501bfd73eb36f2b.woff2 +0 -0
  129. package/dist/remotion-bundle/854236a8376093fe.woff2 +0 -0
  130. package/dist/remotion-bundle/8571d74529082753.woff2 +0 -0
  131. package/dist/remotion-bundle/860bf44f8e6f4b5d.woff2 +0 -0
  132. package/dist/remotion-bundle/879.bundle.js +64 -0
  133. package/dist/remotion-bundle/879.bundle.js.map +1 -0
  134. package/dist/remotion-bundle/887dd482f848d56f.woff2 +0 -0
  135. package/dist/remotion-bundle/89b2132e85fbbb5a.woff2 +0 -0
  136. package/dist/remotion-bundle/8ba60d6c306010c2.woff2 +0 -0
  137. package/dist/remotion-bundle/8c7c4dadea897806.woff2 +0 -0
  138. package/dist/remotion-bundle/8c943f9999706f61.woff2 +0 -0
  139. package/dist/remotion-bundle/8f2a718c90575cc9.woff2 +0 -0
  140. package/dist/remotion-bundle/906b6edb3e1772c9.woff2 +0 -0
  141. package/dist/remotion-bundle/930ff9daccdf14eb.woff2 +0 -0
  142. package/dist/remotion-bundle/934db2f1c403c4d0.woff2 +0 -0
  143. package/dist/remotion-bundle/938.bundle.js +451 -0
  144. package/dist/remotion-bundle/938.bundle.js.map +1 -0
  145. package/dist/remotion-bundle/967.bundle.js +4462 -0
  146. package/dist/remotion-bundle/967.bundle.js.map +1 -0
  147. package/dist/remotion-bundle/9684a1093d3c02ce.woff2 +0 -0
  148. package/dist/remotion-bundle/973dcd0faa6116cc.woff2 +0 -0
  149. package/dist/remotion-bundle/9745400694e76cd8.woff2 +0 -0
  150. package/dist/remotion-bundle/999ef957bed3bdca.woff2 +0 -0
  151. package/dist/remotion-bundle/99a3d67c8b0f43e3.woff2 +0 -0
  152. package/dist/remotion-bundle/a0586c3e03127283.woff2 +0 -0
  153. package/dist/remotion-bundle/a0eb654fdae46269.woff2 +0 -0
  154. package/dist/remotion-bundle/a20e35d3b08f7994.woff2 +0 -0
  155. package/dist/remotion-bundle/a2dcaced7c8c25ab.woff2 +0 -0
  156. package/dist/remotion-bundle/a79255a972a2681a.woff2 +0 -0
  157. package/dist/remotion-bundle/a804b352cb9fec1a.woff2 +0 -0
  158. package/dist/remotion-bundle/aae7117164e1eabc.woff2 +0 -0
  159. package/dist/remotion-bundle/affd121385d0442d.woff2 +0 -0
  160. package/dist/remotion-bundle/b19a6083987ee0d7.woff2 +0 -0
  161. package/dist/remotion-bundle/b1b2bd04d8637981.woff2 +0 -0
  162. package/dist/remotion-bundle/b2c07f341486be87.woff2 +0 -0
  163. package/dist/remotion-bundle/b33d8f82e575c4ce.woff2 +0 -0
  164. package/dist/remotion-bundle/b366c0bed35ef491.woff2 +0 -0
  165. package/dist/remotion-bundle/b41e857ec1b85642.woff2 +0 -0
  166. package/dist/remotion-bundle/b420bb34ccf23e7f.woff2 +0 -0
  167. package/dist/remotion-bundle/b4f7bf4efb0c0ccf.woff2 +0 -0
  168. package/dist/remotion-bundle/b60fe5eca03cff93.woff2 +0 -0
  169. package/dist/remotion-bundle/b6bd31a336e64bce.woff2 +0 -0
  170. package/dist/remotion-bundle/b6d2befba3dfefeb.woff2 +0 -0
  171. package/dist/remotion-bundle/b75f39ab06c43bf4.woff2 +0 -0
  172. package/dist/remotion-bundle/b77880e8c413d4fd.woff2 +0 -0
  173. package/dist/remotion-bundle/b7e38ec441e4a77a.woff2 +0 -0
  174. package/dist/remotion-bundle/b83baa383ff0bf2b.woff2 +0 -0
  175. package/dist/remotion-bundle/b9ad7b6c0a11450a.woff2 +0 -0
  176. package/dist/remotion-bundle/baf84486e8ae3aaf.woff2 +0 -0
  177. package/dist/remotion-bundle/bc047b1f6869cffa.woff2 +0 -0
  178. package/dist/remotion-bundle/bf4f3ac6e93f33aa.woff2 +0 -0
  179. package/dist/remotion-bundle/bf6835ffec5897a2.woff2 +0 -0
  180. package/dist/remotion-bundle/bf8885f581eb1724.woff2 +0 -0
  181. package/dist/remotion-bundle/bundle.js +83376 -0
  182. package/dist/remotion-bundle/bundle.js.map +1 -0
  183. package/dist/remotion-bundle/c03f046bccd789d0.woff2 +0 -0
  184. package/dist/remotion-bundle/c0bb1f8962b73bc3.woff2 +0 -0
  185. package/dist/remotion-bundle/c1003f9a7db6e1cf.woff2 +0 -0
  186. package/dist/remotion-bundle/c15d83fb1e199515.woff2 +0 -0
  187. package/dist/remotion-bundle/c28e7e5d310f73ef.woff2 +0 -0
  188. package/dist/remotion-bundle/c2b840274db78aea.woff2 +0 -0
  189. package/dist/remotion-bundle/c3000e3299d4e45f.woff2 +0 -0
  190. package/dist/remotion-bundle/c83ce886e5288510.woff2 +0 -0
  191. package/dist/remotion-bundle/c87a5a64d4ac0918.woff2 +0 -0
  192. package/dist/remotion-bundle/c8a7e0d049e965fa.woff2 +0 -0
  193. package/dist/remotion-bundle/c949a35d3a3b1faf.woff2 +0 -0
  194. package/dist/remotion-bundle/c9618c9b9ac2bc78.woff2 +0 -0
  195. package/dist/remotion-bundle/ca3add3b84152d5b.woff2 +0 -0
  196. package/dist/remotion-bundle/cad9dd036408d707.woff2 +0 -0
  197. package/dist/remotion-bundle/cbb24916619df439.woff2 +0 -0
  198. package/dist/remotion-bundle/cc054f0b5514e177.woff2 +0 -0
  199. package/dist/remotion-bundle/ccc248ed9312bc71.woff2 +0 -0
  200. package/dist/remotion-bundle/cd9d623aa07af925.woff2 +0 -0
  201. package/dist/remotion-bundle/ce2ba7a321bd1247.woff2 +0 -0
  202. package/dist/remotion-bundle/cf72455f79a29b14.woff2 +0 -0
  203. package/dist/remotion-bundle/d267cbfefab452ac.woff2 +0 -0
  204. package/dist/remotion-bundle/d435cff46a64955f.woff +0 -0
  205. package/dist/remotion-bundle/d494d07f67e363f6.woff2 +0 -0
  206. package/dist/remotion-bundle/d7aa0cc1fa47bf38.woff2 +0 -0
  207. package/dist/remotion-bundle/d7c5ca93d885160a.woff2 +0 -0
  208. package/dist/remotion-bundle/d855d3e252db74e2.woff2 +0 -0
  209. package/dist/remotion-bundle/d8f13d47f02f82c2.woff2 +0 -0
  210. package/dist/remotion-bundle/d9567cce2ee11019.woff2 +0 -0
  211. package/dist/remotion-bundle/db8d4456fc75dd86.woff +0 -0
  212. package/dist/remotion-bundle/dc274628378c47ee.woff2 +0 -0
  213. package/dist/remotion-bundle/dc3e06947bb69903.woff2 +0 -0
  214. package/dist/remotion-bundle/dd67040ac3b6d523.woff2 +0 -0
  215. package/dist/remotion-bundle/e0b04bd488f953f4.woff2 +0 -0
  216. package/dist/remotion-bundle/e2a572ff95089370.woff2 +0 -0
  217. package/dist/remotion-bundle/e2e18a86b1c2b0cc.woff2 +0 -0
  218. package/dist/remotion-bundle/e3a78ee2fc9c6931.woff2 +0 -0
  219. package/dist/remotion-bundle/e654c9d547605a9f.woff2 +0 -0
  220. package/dist/remotion-bundle/e67a3a64c129927c.woff2 +0 -0
  221. package/dist/remotion-bundle/e6be28b4203cd6ce.woff2 +0 -0
  222. package/dist/remotion-bundle/e841907ad9b0a191.woff +0 -0
  223. package/dist/remotion-bundle/e889d1541c69fffa.woff2 +0 -0
  224. package/dist/remotion-bundle/e88ef8c76373a9e2.woff2 +0 -0
  225. package/dist/remotion-bundle/e9c72f4bc37defef.woff2 +0 -0
  226. package/dist/remotion-bundle/e9e35f863403a255.woff2 +0 -0
  227. package/dist/remotion-bundle/eb23b37b009375da.woff2 +0 -0
  228. package/dist/remotion-bundle/ee1342b741625721.woff2 +0 -0
  229. package/dist/remotion-bundle/f07da88543a57ec9.woff2 +0 -0
  230. package/dist/remotion-bundle/f522982115306f8a.woff2 +0 -0
  231. package/dist/remotion-bundle/f8449bd864e6d8bc.woff2 +0 -0
  232. package/dist/remotion-bundle/f906dd5bd95ff9ab.woff2 +0 -0
  233. package/dist/remotion-bundle/f9e9e9413e3c38bb.woff2 +0 -0
  234. package/dist/remotion-bundle/fa5a5b16280994a8.woff2 +0 -0
  235. package/dist/remotion-bundle/favicon.ico +0 -0
  236. package/dist/remotion-bundle/fb19c0517725599b.woff2 +0 -0
  237. package/dist/remotion-bundle/fcaf24232f684b9b.woff2 +0 -0
  238. package/dist/remotion-bundle/fe09e084a3eea8cf.woff2 +0 -0
  239. package/dist/remotion-bundle/ff38d5317df7345a.woff2 +0 -0
  240. package/dist/remotion-bundle/ffe7ea1ea08f455a.woff2 +0 -0
  241. package/dist/remotion-bundle/index.html +49 -0
  242. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/0.mp3 +0 -0
  243. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/1.mp3 +0 -0
  244. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/2.mp3 +0 -0
  245. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/3.mp3 +0 -0
  246. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/0.mp3 +0 -0
  247. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/1.mp3 +0 -0
  248. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/2.mp3 +0 -0
  249. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/3.mp3 +0 -0
  250. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/0.mp3 +0 -0
  251. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/1.mp3 +0 -0
  252. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/2.mp3 +0 -0
  253. package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/3.mp3 +0 -0
  254. package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/0.mp3 +0 -0
  255. package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/1.mp3 +0 -0
  256. package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/2.mp3 +0 -0
  257. package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/3.mp3 +0 -0
  258. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/0.mp3 +0 -0
  259. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/1.mp3 +0 -0
  260. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/2.mp3 +0 -0
  261. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/3.mp3 +0 -0
  262. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/0.mp3 +0 -0
  263. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/1.mp3 +0 -0
  264. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/2.mp3 +0 -0
  265. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/3.mp3 +0 -0
  266. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/0.mp3 +0 -0
  267. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/1.mp3 +0 -0
  268. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/2.mp3 +0 -0
  269. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/3.mp3 +0 -0
  270. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/0.mp3 +0 -0
  271. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/1.mp3 +0 -0
  272. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/2.mp3 +0 -0
  273. package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/3.mp3 +0 -0
  274. package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/0.mp3 +0 -0
  275. package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/1.mp3 +0 -0
  276. package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/2.mp3 +0 -0
  277. package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/3.mp3 +0 -0
  278. package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/0.mp3 +0 -0
  279. package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/1.mp3 +0 -0
  280. package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/2.mp3 +0 -0
  281. package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/3.mp3 +0 -0
  282. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/0.mp3 +0 -0
  283. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/1.mp3 +0 -0
  284. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/2.mp3 +0 -0
  285. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/3.mp3 +0 -0
  286. package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/4.mp3 +0 -0
  287. package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/0.mp3 +0 -0
  288. package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/1.mp3 +0 -0
  289. package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/2.mp3 +0 -0
  290. package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/3.mp3 +0 -0
  291. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/0.mp3 +0 -0
  292. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/1.mp3 +0 -0
  293. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/2.mp3 +0 -0
  294. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/3.mp3 +0 -0
  295. package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/4.mp3 +0 -0
  296. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/0.mp3 +0 -0
  297. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/1.mp3 +0 -0
  298. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/2.mp3 +0 -0
  299. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/3.mp3 +0 -0
  300. package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/4.mp3 +0 -0
  301. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/0.mp3 +0 -0
  302. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/1.mp3 +0 -0
  303. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/2.mp3 +0 -0
  304. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/3.mp3 +0 -0
  305. package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/4.mp3 +0 -0
  306. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/0.mp3 +0 -0
  307. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/1.mp3 +0 -0
  308. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/2.mp3 +0 -0
  309. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/3.mp3 +0 -0
  310. package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/4.mp3 +0 -0
  311. package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/0.mp3 +0 -0
  312. package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/1.mp3 +0 -0
  313. package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/2.mp3 +0 -0
  314. package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/3.mp3 +0 -0
  315. package/dist/remotion-bundle/public/paper-slide-experiments/product-update/0.mp3 +0 -0
  316. package/dist/remotion-bundle/public/paper-slide-experiments/product-update/1.mp3 +0 -0
  317. package/dist/remotion-bundle/public/paper-slide-experiments/product-update/2.mp3 +0 -0
  318. package/dist/remotion-bundle/public/paper-slide-experiments/product-update/3.mp3 +0 -0
  319. package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/0.mp3 +0 -0
  320. package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/1.mp3 +0 -0
  321. package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/2.mp3 +0 -0
  322. package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/3.mp3 +0 -0
  323. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/0.mp3 +0 -0
  324. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/1.mp3 +0 -0
  325. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/2.mp3 +0 -0
  326. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/3.mp3 +0 -0
  327. package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/4.mp3 +0 -0
  328. package/dist/remotion-bundle/public/voiceover/ai-life/card-0.mp3 +0 -0
  329. package/dist/remotion-bundle/public/voiceover/ai-life/card-1.mp3 +0 -0
  330. package/dist/remotion-bundle/public/voiceover/ai-life/card-2.mp3 +0 -0
  331. package/dist/remotion-bundle/public/voiceover/ai-life/card-3.mp3 +0 -0
  332. package/dist/remotion-bundle/public/voiceover/ai-life/card-4.mp3 +0 -0
  333. package/dist/remotion-bundle/public/voiceover/ai-life/card-5.mp3 +0 -0
  334. package/dist/remotion-bundle/public/voiceover/coffee-science/card-0.mp3 +0 -0
  335. package/dist/remotion-bundle/public/voiceover/coffee-science/card-1.mp3 +0 -0
  336. package/dist/remotion-bundle/public/voiceover/coffee-science/card-2.mp3 +0 -0
  337. package/dist/remotion-bundle/public/voiceover/coffee-science/card-3.mp3 +0 -0
  338. package/dist/remotion-bundle/public/voiceover/coffee-science/card-4.mp3 +0 -0
  339. package/dist/remotion-bundle/public/voiceover/coffee-science/card-5.mp3 +0 -0
  340. package/dist/remotion-bundle/public/voiceover/coffee-science/card-6.mp3 +0 -0
  341. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-0.mp3 +0 -0
  342. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-1.mp3 +0 -0
  343. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-2.mp3 +0 -0
  344. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-3.mp3 +0 -0
  345. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-4.mp3 +0 -0
  346. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-5.mp3 +0 -0
  347. package/dist/remotion-bundle/public/voiceover/reading-secrets/card-6.mp3 +0 -0
  348. package/dist/remotion-bundle/public/voiceover/remote-work/card-0.mp3 +0 -0
  349. package/dist/remotion-bundle/public/voiceover/remote-work/card-1.mp3 +0 -0
  350. package/dist/remotion-bundle/public/voiceover/remote-work/card-2.mp3 +0 -0
  351. package/dist/remotion-bundle/public/voiceover/remote-work/card-3.mp3 +0 -0
  352. package/dist/remotion-bundle/public/voiceover/remote-work/card-4.mp3 +0 -0
  353. package/dist/remotion-bundle/public/voiceover/remote-work/card-5.mp3 +0 -0
  354. package/dist/remotion-bundle/source-map-helper.wasm +0 -0
  355. package/lib/cli.js +270 -0
  356. package/lib/commands/_registry.js +48 -0
  357. package/lib/commands/add.js +242 -0
  358. package/lib/commands/asr/azure-transcribe.js +336 -0
  359. package/lib/commands/asr/cloud-transcribe.js +384 -0
  360. package/lib/commands/asr/helpers.js +76 -0
  361. package/lib/commands/asr/index.js +236 -0
  362. package/lib/commands/asr/local-transcribe.js +125 -0
  363. package/lib/commands/asr-jobs.js +257 -0
  364. package/lib/commands/asr.js +11 -0
  365. package/lib/commands/auth-cmds.js +358 -0
  366. package/lib/commands/dub.js +542 -0
  367. package/lib/commands/explain.js +512 -0
  368. package/lib/commands/feedback.js +152 -0
  369. package/lib/commands/image.js +207 -0
  370. package/lib/commands/mcp-key.js +166 -0
  371. package/lib/commands/narrate.js +639 -0
  372. package/lib/commands/picstory-templates.js +276 -0
  373. package/lib/commands/picstory.js +547 -0
  374. package/lib/commands/podcast/dialogue.js +109 -0
  375. package/lib/commands/podcast/generate.js +127 -0
  376. package/lib/commands/podcast/index.js +561 -0
  377. package/lib/commands/podcast/synthesize.js +188 -0
  378. package/lib/commands/podcast.js +11 -0
  379. package/lib/commands/present.js +519 -0
  380. package/lib/commands/publish.js +415 -0
  381. package/lib/commands/skills.js +473 -0
  382. package/lib/commands/slice-preview.js +266 -0
  383. package/lib/commands/slice-render.js +282 -0
  384. package/lib/commands/slice-stage.js +264 -0
  385. package/lib/commands/slice.js +343 -0
  386. package/lib/commands/slides/constants.js +108 -0
  387. package/lib/commands/slides/html-renderer.js +338 -0
  388. package/lib/commands/slides/index.js +345 -0
  389. package/lib/commands/slides.js +11 -0
  390. package/lib/commands/story.js +302 -0
  391. package/lib/commands/summarize.js +532 -0
  392. package/lib/commands/synthesize.js +261 -0
  393. package/lib/commands/translate.js +593 -0
  394. package/lib/commands/upgrade.js +249 -0
  395. package/lib/commands/video-translate.js +577 -0
  396. package/lib/commands/voices.js +292 -0
  397. package/lib/core/agent-env.js +104 -0
  398. package/lib/core/args.js +107 -0
  399. package/lib/core/asr-client.js +448 -0
  400. package/lib/core/asr-jobs-client.js +126 -0
  401. package/lib/core/asr-jobs-store.js +105 -0
  402. package/lib/core/asr-r2-upload.js +181 -0
  403. package/lib/core/asr-upload.js +132 -0
  404. package/lib/core/audio-extract.js +150 -0
  405. package/lib/core/audio.js +219 -0
  406. package/lib/core/auth.js +880 -0
  407. package/lib/core/config.js +197 -0
  408. package/lib/core/feedback.js +64 -0
  409. package/lib/core/ffmpeg.js +476 -0
  410. package/lib/core/http.js +188 -0
  411. package/lib/core/image-client.js +55 -0
  412. package/lib/core/intent-params.js +11 -0
  413. package/lib/core/llm-client.js +76 -0
  414. package/lib/core/logger.js +208 -0
  415. package/lib/core/mic-recorder.js +182 -0
  416. package/lib/core/pause-markers.js +94 -0
  417. package/lib/core/podcast-pacing.js +118 -0
  418. package/lib/core/spinner.js +33 -0
  419. package/lib/core/srt.js +394 -0
  420. package/lib/core/telemetry.js +100 -0
  421. package/lib/core/timeline.js +92 -0
  422. package/lib/core/tts-synthesizer.js +70 -0
  423. package/lib/core/update-check.js +185 -0
  424. package/lib/core/url-download.js +148 -0
  425. package/lib/core/whisper-local.js +279 -0
  426. package/lib/internal/deck-validator.js +488 -0
  427. package/lib/internal/slice-themes.json +370 -0
  428. package/lib/stage-core/cloud-render.js +170 -0
  429. package/lib/stage-core/deck-format.js +133 -0
  430. package/lib/stage-core/edit-prompt.js +104 -0
  431. package/lib/stage-core/event-bus.js +31 -0
  432. package/lib/stage-core/port.js +46 -0
  433. package/lib/stage-core/server.js +352 -0
  434. package/lib/stage-core/snapshot-store.js +198 -0
  435. package/lib/stage-core/watcher.js +106 -0
  436. package/lib/stage-ui/slice/template.js +1672 -0
  437. package/package.json +9 -4
  438. package/skills/.claude-plugin/marketplace.json +22 -0
  439. package/skills/.claude-plugin/plugin.json +25 -0
  440. package/skills/LICENSE +21 -0
  441. package/skills/README.md +120 -0
  442. package/skills/hub/SKILL.md +317 -0
  443. package/skills/podcast/SKILL.md +146 -0
  444. package/skills/slice/SKILL.md +205 -0
  445. package/skills/slice/agents/openai.yaml +4 -0
  446. package/skills/slice/references/deck-schema.md +183 -0
  447. package/skills/slice/references/example-decks.md +108 -0
  448. package/skills/slice/references/themes.md +172 -0
  449. package/skills/transcribe/SKILL.md +473 -0
  450. package/skills/video/SKILL.md +261 -0
  451. package/skills/voxflow-slice/SKILL.md +271 -0
  452. package/skills/voxflow-slice/examples/article.md +13 -0
  453. package/skills/voxflow-slice/examples/expected-deck.json +39 -0
  454. package/skills/voxflow-slice/examples/validate.mjs +46 -0
@@ -0,0 +1,476 @@
1
+ /**
2
+ * VoxFlow CLI — FFmpeg utility layer
3
+ *
4
+ * Provides ffmpeg/ffprobe operations via child_process.execFile.
5
+ * Zero external dependencies — requires ffmpeg to be installed on PATH.
6
+ * Also provides a one-time dependency hint for users without FFmpeg.
7
+ */
8
+
9
+ const { execFile } = require('child_process');
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+
13
+ /**
14
+ * Run a command via execFile and return { stdout, stderr }.
15
+ * Rejects on non-zero exit code or spawn error.
16
+ * @param {string} cmd
17
+ * @param {string[]} args
18
+ * @param {object} [opts]
19
+ * @returns {Promise<{stdout: string, stderr: string}>}
20
+ */
21
+ function runCommand(cmd, args, opts) {
22
+ return new Promise((resolve, reject) => {
23
+ execFile(cmd, args, { timeout: 600_000, ...opts }, (error, stdout, stderr) => {
24
+ if (error) {
25
+ error.stderr = stderr;
26
+ error.stdout = stdout;
27
+ reject(error);
28
+ } else {
29
+ resolve({ stdout, stderr });
30
+ }
31
+ });
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Check if ffmpeg and ffprobe are available.
37
+ * @returns {Promise<{available: boolean, version?: string, ffprobeAvailable?: boolean}>}
38
+ */
39
+ async function checkFfmpeg() {
40
+ try {
41
+ const { stdout } = await runCommand('ffmpeg', ['-version']);
42
+ const versionMatch = stdout.match(/ffmpeg version (\S+)/);
43
+ const version = versionMatch ? versionMatch[1] : 'unknown';
44
+
45
+ // Also check ffprobe
46
+ let ffprobeOk = false;
47
+ try {
48
+ await runCommand('ffprobe', ['-version']);
49
+ ffprobeOk = true;
50
+ } catch {
51
+ // ffprobe not available
52
+ }
53
+
54
+ return {
55
+ available: true,
56
+ version,
57
+ ffprobeAvailable: ffprobeOk,
58
+ };
59
+ } catch {
60
+ return { available: false };
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Get the duration of an audio or video file in milliseconds.
66
+ * Uses ffprobe to extract duration.
67
+ * @param {string} filePath - Path to audio/video file
68
+ * @returns {Promise<number>} Duration in milliseconds
69
+ */
70
+ async function getAudioDuration(filePath) {
71
+ const resolved = path.resolve(filePath);
72
+ try {
73
+ const { stdout } = await runCommand('ffprobe', [
74
+ '-v', 'error',
75
+ '-show_entries', 'format=duration',
76
+ '-of', 'default=noprint_wrappers=1:nokey=1',
77
+ resolved,
78
+ ]);
79
+
80
+ const seconds = parseFloat(stdout.trim());
81
+ if (isNaN(seconds)) {
82
+ throw new Error(`Could not parse duration from ffprobe output: "${stdout.trim()}"`);
83
+ }
84
+ return Math.round(seconds * 1000);
85
+ } catch (err) {
86
+ if (err.code === 'ENOENT') {
87
+ throw new Error('ffprobe not found. Please install ffmpeg: https://ffmpeg.org/download.html');
88
+ }
89
+ throw new Error(`Failed to get duration of ${filePath}: ${err.message}`);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Extract audio track from a video file as WAV (24kHz, 16-bit, mono).
95
+ * @param {string} videoPath - Input video file
96
+ * @param {string} outputPath - Output WAV file path
97
+ * @returns {Promise<string>} Output file path
98
+ */
99
+ async function extractAudio(videoPath, outputPath) {
100
+ const resolvedVideo = path.resolve(videoPath);
101
+ const resolvedOutput = path.resolve(outputPath);
102
+
103
+ try {
104
+ await runCommand('ffmpeg', [
105
+ '-i', resolvedVideo,
106
+ '-vn', // no video
107
+ '-acodec', 'pcm_s16le', // 16-bit PCM
108
+ '-ar', '24000', // 24kHz sample rate
109
+ '-ac', '1', // mono
110
+ '-y', // overwrite
111
+ resolvedOutput,
112
+ ]);
113
+ return resolvedOutput;
114
+ } catch (err) {
115
+ if (err.code === 'ENOENT') {
116
+ throw new Error('ffmpeg not found. Please install ffmpeg: https://ffmpeg.org/download.html');
117
+ }
118
+ throw new Error(`Failed to extract audio from ${videoPath}: ${err.stderr || err.message}`);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Merge an audio track into a video file, replacing original audio.
124
+ * @param {string} videoPath - Input video file
125
+ * @param {string} audioPath - Input audio file (WAV/MP3)
126
+ * @param {string} outputPath - Output video file path
127
+ * @returns {Promise<string>} Output file path
128
+ */
129
+ async function mergeAudioVideo(videoPath, audioPath, outputPath) {
130
+ const resolvedVideo = path.resolve(videoPath);
131
+ const resolvedAudio = path.resolve(audioPath);
132
+ const resolvedOutput = path.resolve(outputPath);
133
+
134
+ try {
135
+ await runCommand('ffmpeg', [
136
+ '-i', resolvedVideo,
137
+ '-i', resolvedAudio,
138
+ '-c:v', 'copy', // copy video stream
139
+ '-map', '0:v:0', // video from first input
140
+ '-map', '1:a:0', // audio from second input
141
+ '-shortest', // match shortest stream
142
+ '-y', // overwrite
143
+ resolvedOutput,
144
+ ]);
145
+ return resolvedOutput;
146
+ } catch (err) {
147
+ if (err.code === 'ENOENT') {
148
+ throw new Error('ffmpeg not found. Please install ffmpeg: https://ffmpeg.org/download.html');
149
+ }
150
+ throw new Error(`Failed to merge audio/video: ${err.stderr || err.message}`);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Mix main audio with background music using volume ducking.
156
+ * BGM is reduced to the ducking level and mixed under the main audio.
157
+ * @param {string} mainAudioPath - Main audio (dubbing voice)
158
+ * @param {string} bgmPath - Background music file
159
+ * @param {string} outputPath - Output mixed audio path
160
+ * @param {object} [opts]
161
+ * @param {number} [opts.ducking=0.2] - BGM volume level (0-1, default 0.2 = 20%)
162
+ * @returns {Promise<string>} Output file path
163
+ */
164
+ async function mixWithBgm(mainAudioPath, bgmPath, outputPath, opts = {}) {
165
+ const ducking = opts.ducking ?? 0.2;
166
+ const resolvedMain = path.resolve(mainAudioPath);
167
+ const resolvedBgm = path.resolve(bgmPath);
168
+ const resolvedOutput = path.resolve(outputPath);
169
+
170
+ try {
171
+ await runCommand('ffmpeg', [
172
+ '-i', resolvedMain,
173
+ '-i', resolvedBgm,
174
+ '-filter_complex',
175
+ `[1:a]volume=${ducking}[bgm_low];` +
176
+ `[0:a][bgm_low]amix=inputs=2:duration=first:dropout_transition=2[out]`,
177
+ '-map', '[out]',
178
+ '-acodec', 'pcm_s16le',
179
+ '-ar', '24000',
180
+ '-ac', '1',
181
+ '-y',
182
+ resolvedOutput,
183
+ ]);
184
+ return resolvedOutput;
185
+ } catch (err) {
186
+ if (err.code === 'ENOENT') {
187
+ throw new Error('ffmpeg not found. Please install ffmpeg: https://ffmpeg.org/download.html');
188
+ }
189
+ throw new Error(`Failed to mix audio with BGM: ${err.stderr || err.message}`);
190
+ }
191
+ }
192
+
193
+ // ─── Dependency hint ────────────────────────────────────────────────────────
194
+
195
+ /**
196
+ * Show a one-time friendly hint if FFmpeg is not installed.
197
+ * Does NOT block execution — just prints a warning once.
198
+ * The hint is suppressed after the first display (flag stored in config dir).
199
+ *
200
+ * @param {string} configDir - Path to the config directory (e.g. ~/.config/voxflow)
201
+ * @param {string} command - The command being run (e.g. 'dub', 'asr')
202
+ * @returns {Promise<{available: boolean, version?: string}>}
203
+ */
204
+ async function warnIfMissingFfmpeg(configDir, command) {
205
+ const info = await checkFfmpeg();
206
+ if (info.available) return info;
207
+
208
+ // Check if we already showed the hint
209
+ const hintFlag = path.join(configDir, '.ffmpeg-hint-shown');
210
+ try {
211
+ if (fs.existsSync(hintFlag)) return info;
212
+ } catch {
213
+ // ignore fs errors, just show the hint
214
+ }
215
+
216
+ // Show the hint
217
+ const features = {
218
+ dub: 'video merging (--video), BGM mixing (--bgm), speed adjustment (--speed-auto)',
219
+ asr: 'audio format conversion, video audio extraction',
220
+ };
221
+ const featureDesc = features[command] || 'audio/video processing';
222
+
223
+ console.log(
224
+ '\n\x1b[33m' +
225
+ `[hint] ffmpeg not found — needed for ${featureDesc}.\n` +
226
+ ' Install: brew install ffmpeg (macOS) / sudo apt install ffmpeg (Linux)\n' +
227
+ ' Without ffmpeg, some features will be unavailable.\x1b[0m\n'
228
+ );
229
+
230
+ // Persist the flag so the hint only shows once
231
+ try {
232
+ fs.mkdirSync(configDir, { recursive: true });
233
+ fs.writeFileSync(hintFlag, new Date().toISOString(), 'utf8');
234
+ } catch {
235
+ // non-critical — next run will show hint again
236
+ }
237
+
238
+ return info;
239
+ }
240
+
241
+ // ─── Video summarize helpers ────────────────────────────────────────────────
242
+
243
+ /**
244
+ * Cut a segment from a video file.
245
+ * @param {string} input - Input video path
246
+ * @param {string} output - Output segment path
247
+ * @param {number} startSec - Start time in seconds
248
+ * @param {number} endSec - End time in seconds
249
+ */
250
+ async function cutVideo(input, output, startSec, endSec) {
251
+ await runCommand('ffmpeg', [
252
+ '-i', path.resolve(input),
253
+ '-ss', String(startSec),
254
+ '-to', String(endSec),
255
+ '-c', 'copy',
256
+ '-y', path.resolve(output),
257
+ ]);
258
+ }
259
+
260
+ /**
261
+ * Overlay text on video with a semi-transparent bottom bar.
262
+ * @param {string} input - Input video path
263
+ * @param {string} output - Output video path
264
+ * @param {string} text - Text to display
265
+ * @param {object} [opts]
266
+ * @param {string} [opts.fontfile] - Path to font file
267
+ * @param {number} [opts.fontSize=28] - Font size
268
+ */
269
+ async function overlayText(input, output) {
270
+ // Copy through — text overlay requires drawtext (libfreetype).
271
+ // Future: install FFmpeg with --enable-libfreetype for text overlays.
272
+ fs.copyFileSync(path.resolve(input), path.resolve(output));
273
+ }
274
+
275
+ /**
276
+ * Generate a title card video (solid color background + centered text).
277
+ * @param {string} output - Output video path
278
+ * @param {string} text - Title text
279
+ * @param {object} [opts]
280
+ * @param {number} [opts.duration=3] - Duration in seconds
281
+ * @param {string} [opts.bgColor='0x0F172A'] - Background color
282
+ * @param {string} [opts.resolution='1920x1080'] - Video resolution
283
+ * @param {string} [opts.fontfile] - Path to font file
284
+ * @param {number} [opts.fontSize=52] - Font size
285
+ */
286
+ /**
287
+ * Generate a title card from the first frame of a video (darkened + blurred).
288
+ * Falls back to solid color if no source video is provided.
289
+ * No drawtext/libfreetype needed — narration provides the context.
290
+ *
291
+ * @param {string} output - Output video path
292
+ * @param {string} text - Title text (unused — reserved for future drawtext support)
293
+ * @param {object} [opts]
294
+ * @param {number} [opts.duration=3] - Duration in seconds
295
+ * @param {string} [opts.sourceVideo] - Source video to extract first frame from
296
+ * @param {string} [opts.bgColor='0x0F172A'] - Fallback background color
297
+ * @param {string} [opts.resolution='1920x1080'] - Video resolution
298
+ */
299
+ async function generateTitleCard(output, text, opts = {}) {
300
+ const duration = opts.duration || 3;
301
+ const resolution = opts.resolution || '1920x1080';
302
+
303
+ if (opts.sourceVideo && fs.existsSync(path.resolve(opts.sourceVideo))) {
304
+ // Extract first frame, darken + slight blur → cinematic title card
305
+ const tmpFrame = output + '.frame.jpg';
306
+ try {
307
+ await runCommand('ffmpeg', [
308
+ '-i', path.resolve(opts.sourceVideo),
309
+ '-vframes', '1', '-q:v', '2',
310
+ '-y', tmpFrame,
311
+ ]);
312
+
313
+ // Loop the darkened frame as a video clip with silent audio
314
+ await runCommand('ffmpeg', [
315
+ '-loop', '1', '-i', tmpFrame,
316
+ '-f', 'lavfi', '-i', 'anullsrc=r=24000:cl=mono',
317
+ '-t', String(duration),
318
+ '-vf', 'eq=brightness=-0.4:saturation=0.6,boxblur=8:2',
319
+ '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'fast',
320
+ '-c:a', 'aac', '-shortest',
321
+ '-y', path.resolve(output),
322
+ ]);
323
+ return;
324
+ } catch {
325
+ // Fall through to solid color
326
+ } finally {
327
+ try { fs.unlinkSync(tmpFrame); } catch {}
328
+ }
329
+ }
330
+
331
+ // Fallback: solid color card
332
+ const bgColor = opts.bgColor || '0x0F172A';
333
+ await runCommand('ffmpeg', [
334
+ '-f', 'lavfi', '-i', `color=c=${bgColor}:s=${resolution}:d=${duration}`,
335
+ '-f', 'lavfi', '-i', 'anullsrc=r=24000:cl=mono',
336
+ '-c:v', 'libx264', '-pix_fmt', 'yuv420p',
337
+ '-c:a', 'aac', '-shortest',
338
+ '-y', path.resolve(output),
339
+ ]);
340
+ }
341
+
342
+ /**
343
+ * Replace the audio track of a video with a new audio file.
344
+ * @param {string} videoInput - Input video path
345
+ * @param {string} audioInput - New audio file (WAV)
346
+ * @param {string} output - Output video path
347
+ */
348
+ async function replaceAudio(videoInput, audioInput, output) {
349
+ await runCommand('ffmpeg', [
350
+ '-i', path.resolve(videoInput),
351
+ '-i', path.resolve(audioInput),
352
+ '-c:v', 'copy',
353
+ '-map', '0:v:0',
354
+ '-map', '1:a:0',
355
+ '-shortest',
356
+ '-y', path.resolve(output),
357
+ ]);
358
+ }
359
+
360
+ /**
361
+ * Concatenate multiple video files using the concat demuxer.
362
+ * All inputs must have the same codec, resolution, and frame rate.
363
+ * @param {string[]} inputs - Array of input video file paths
364
+ * @param {string} output - Output concatenated video path
365
+ */
366
+ async function concatVideos(inputs, output) {
367
+ const tmpDir = path.dirname(output);
368
+ const listFile = path.join(tmpDir, `concat-list-${Date.now()}.txt`);
369
+ const content = inputs.map(f => `file '${path.resolve(f)}'`).join('\n');
370
+ fs.writeFileSync(listFile, content, 'utf8');
371
+
372
+ try {
373
+ await runCommand('ffmpeg', [
374
+ '-f', 'concat', '-safe', '0',
375
+ '-i', listFile,
376
+ '-c', 'copy',
377
+ '-y', path.resolve(output),
378
+ ]);
379
+ } finally {
380
+ try { fs.unlinkSync(listFile); } catch {}
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Re-encode a video to a standard format (H.264 + AAC) for concat compatibility.
386
+ * @param {string} input - Input video path
387
+ * @param {string} output - Output normalized video path
388
+ * @param {object} [opts]
389
+ * @param {string} [opts.resolution] - Target resolution (e.g. '1920x1080')
390
+ */
391
+ async function normalizeVideo(input, output, opts = {}) {
392
+ const args = [
393
+ '-i', path.resolve(input),
394
+ '-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
395
+ '-c:a', 'aac', '-ar', '24000', '-ac', '1',
396
+ '-pix_fmt', 'yuv420p',
397
+ ];
398
+ if (opts.resolution) {
399
+ args.push('-vf', `scale=${opts.resolution.replace('x', ':')}:force_original_aspect_ratio=decrease,pad=${opts.resolution.replace('x', ':')}:(ow-iw)/2:(oh-ih)/2`);
400
+ }
401
+ args.push('-y', path.resolve(output));
402
+ await runCommand('ffmpeg', args, { timeout: 600_000 });
403
+ }
404
+
405
+ /**
406
+ * Detect a suitable CJK font on the current system.
407
+ * @returns {string|null} Font file path or null
408
+ */
409
+ function detectCjkFont() {
410
+ const candidates = [
411
+ // macOS
412
+ '/System/Library/Fonts/PingFang.ttc',
413
+ '/System/Library/Fonts/STHeiti Light.ttc',
414
+ '/System/Library/Fonts/Hiragino Sans GB.ttc',
415
+ // Linux
416
+ '/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc',
417
+ '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
418
+ '/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc',
419
+ // Windows
420
+ 'C:\\Windows\\Fonts\\msyh.ttc',
421
+ 'C:\\Windows\\Fonts\\simhei.ttf',
422
+ ];
423
+ for (const f of candidates) {
424
+ if (fs.existsSync(f)) return f;
425
+ }
426
+ return null;
427
+ }
428
+
429
+ /**
430
+ * Convert an audio file to a different format via ffmpeg.
431
+ * Used when the pipeline produces WAV but the user requested .mp3 output.
432
+ * @param {string} inputPath - Input audio file (e.g. WAV)
433
+ * @param {string} outputPath - Output audio file (e.g. MP3)
434
+ * @returns {Promise<string>} Output file path
435
+ */
436
+ async function convertAudioFormat(inputPath, outputPath) {
437
+ const resolvedInput = path.resolve(inputPath);
438
+ const resolvedOutput = path.resolve(outputPath);
439
+
440
+ if (resolvedInput === resolvedOutput) {
441
+ throw new Error('convertAudioFormat: input and output paths must differ');
442
+ }
443
+
444
+ try {
445
+ await runCommand('ffmpeg', [
446
+ '-i', resolvedInput,
447
+ '-y',
448
+ resolvedOutput,
449
+ ]);
450
+ return resolvedOutput;
451
+ } catch (err) {
452
+ if (err.code === 'ENOENT') {
453
+ throw new Error('ffmpeg not found. Please install ffmpeg: https://ffmpeg.org/download.html');
454
+ }
455
+ throw new Error(`Failed to convert audio format: ${err.stderr || err.message}`);
456
+ }
457
+ }
458
+
459
+ module.exports = {
460
+ runCommand,
461
+ checkFfmpeg,
462
+ getAudioDuration,
463
+ extractAudio,
464
+ mergeAudioVideo,
465
+ mixWithBgm,
466
+ warnIfMissingFfmpeg,
467
+ convertAudioFormat,
468
+ // Video summarize helpers
469
+ cutVideo,
470
+ overlayText,
471
+ generateTitleCard,
472
+ replaceAudio,
473
+ concatVideos,
474
+ normalizeVideo,
475
+ detectCjkFont,
476
+ };
@@ -0,0 +1,188 @@
1
+ /**
2
+ * VoxFlow CLI — HTTP request helper and API error types
3
+ */
4
+
5
+ const http = require('http');
6
+ const https = require('https');
7
+ const crypto = require('crypto');
8
+ const { logger } = require('./logger');
9
+ const pkg = require('../../package.json');
10
+
11
+ // ─── Error types ─────────────────────────────────────────────────────────────
12
+
13
+ class ApiError extends Error {
14
+ constructor(message, code, status, retryAfterSec) {
15
+ super(message);
16
+ this.name = 'ApiError';
17
+ this.code = code;
18
+ this.status = status;
19
+ if (Number.isFinite(retryAfterSec)) this.retryAfterSec = retryAfterSec;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Resolve a Retry-After value from a 429 response.
25
+ * Standard HTTP header (seconds OR HTTP-date) takes precedence; fall back to
26
+ * a `retryAfter` / `retry_after` field in the JSON body, which some VoxFlow
27
+ * routes echo for client convenience.
28
+ *
29
+ * @returns {number|null} seconds to wait, or null if absent / unparseable
30
+ */
31
+ function parseRetryAfterSeconds(headers, data) {
32
+ const headerVal = headers && (headers['retry-after'] || headers['Retry-After']);
33
+ if (headerVal) {
34
+ const numeric = Number(headerVal);
35
+ if (Number.isFinite(numeric) && numeric > 0) return Math.ceil(numeric);
36
+ const dateMs = Date.parse(headerVal);
37
+ if (Number.isFinite(dateMs)) {
38
+ const sec = Math.ceil((dateMs - Date.now()) / 1000);
39
+ if (sec > 0) return sec;
40
+ }
41
+ }
42
+ const body = data?.retryAfter ?? data?.retry_after;
43
+ if (Number.isFinite(body) && body > 0) return Math.ceil(body);
44
+ return null;
45
+ }
46
+
47
+ function throwApiError(status, data, context, headers) {
48
+ if (status === 401) {
49
+ throw new ApiError(
50
+ `Token expired or invalid. Run: voxflow login`,
51
+ 'token_expired',
52
+ 401
53
+ );
54
+ }
55
+ // 402 = Payment Required (quota exhausted) → distinct from 429 (rate limit).
56
+ // Conflating them was misleading: a podcast user with full bonus quota who
57
+ // tripped the per-minute TTS limit (20/min) would see "Monthly quota exceeded"
58
+ // and assume their account was empty. Now we keep them split, and retry-after
59
+ // is surfaced when the server returns it.
60
+ if (status === 402 || (data && data.code === 'quota_exceeded')) {
61
+ const remaining = data?.quotaInfo?.remainingTotal;
62
+ const detail = Number.isFinite(remaining)
63
+ ? ` Remaining: ${remaining.toLocaleString()}.`
64
+ : '';
65
+ throw new ApiError(
66
+ `Insufficient quota for this request.${detail} Top up at https://voxflow.studio/pricing or check: voxflow status`,
67
+ 'insufficient_quota',
68
+ status
69
+ );
70
+ }
71
+ if (status === 429) {
72
+ const ra = parseRetryAfterSeconds(headers, data);
73
+ const wait = ra ? ` Retry after ${ra}s.` : ' Slow down and retry.';
74
+ throw new ApiError(
75
+ `Rate limit hit (too many requests in a short window).${wait} Quota is unaffected.`,
76
+ 'rate_limited',
77
+ 429,
78
+ ra ?? undefined
79
+ );
80
+ }
81
+ if (status >= 500) {
82
+ // Surface the backend's error code and message when available — the
83
+ // older generic "Server error (500). Please try again later." swallowed
84
+ // the actual cause (e.g. LLM provider failure on `slides`, issue #2916)
85
+ // and made it impossible to debug from the CLI alone.
86
+ const backendCode = data?.code;
87
+ const detail = data?.message || data?.error;
88
+ let suffix;
89
+ if (detail || backendCode) {
90
+ const codeLabel = backendCode ? `[${backendCode}] ` : '';
91
+ suffix = `: ${codeLabel}${detail || 'no message from server'}`;
92
+ } else {
93
+ suffix = '. Please try again later.';
94
+ }
95
+ throw new ApiError(
96
+ `Server error (${status})${suffix}`,
97
+ 'server_error',
98
+ status
99
+ );
100
+ }
101
+ const detail = data?.message || data?.code || JSON.stringify(data);
102
+ throw new ApiError(`${context} failed (${status}): ${detail}`, 'api_error', status);
103
+ }
104
+
105
+ function throwNetworkError(err, apiBase) {
106
+ const code = err.code || '';
107
+ if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ETIMEDOUT') {
108
+ throw new ApiError(
109
+ `Cannot reach API server at ${apiBase}. Check your internet connection or try --api <url>`,
110
+ 'network_error',
111
+ 0
112
+ );
113
+ }
114
+ throw err;
115
+ }
116
+
117
+ // ─── HTTP Helper ────────────────────────────────────────────────────────────
118
+
119
+ /**
120
+ * Fire an HTTP request and parse a JSON response.
121
+ *
122
+ * `options.timeoutMs` (default 60s) is the inactivity timeout — long-running
123
+ * synth calls (TTS for a paragraph, LLM slide generation) are expected to
124
+ * exceed the default and pass an explicit larger value.
125
+ */
126
+ function request(url, options, body) {
127
+ return new Promise((resolve, reject) => {
128
+ const parsedUrl = new URL(url);
129
+ const mod = parsedUrl.protocol === 'https:' ? https : http;
130
+
131
+ const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : 60_000;
132
+
133
+ // Inject CLI client source header into every request, plus a
134
+ // per-request UUID for tracing + deduct_quota idempotency (#3194).
135
+ // Caller can override X-Request-Id explicitly to chain a retry to
136
+ // the same logical request — without that, each call gets a fresh
137
+ // ID. crypto.randomUUID is built-in on Node 19+ (we require 20+).
138
+ if (!options.headers) {
139
+ options.headers = {};
140
+ }
141
+ options.headers['X-Client-Source'] = 'cli';
142
+ options.headers['X-VoxFlow-Client'] = `voxflow-cli/${pkg.version}`;
143
+ if (!options.headers['X-Request-Id']) {
144
+ options.headers['X-Request-Id'] = crypto.randomUUID();
145
+ }
146
+
147
+ const startMs = Date.now();
148
+ const method = (options.method || 'GET').toUpperCase();
149
+ // Trim path query down to something greppable without dumping auth tokens.
150
+ logger.debug({ method, host: parsedUrl.host, path: parsedUrl.pathname }, 'http request');
151
+
152
+ const req = mod.request(parsedUrl, options, (res) => {
153
+ const chunks = [];
154
+ res.on('data', (chunk) => chunks.push(chunk));
155
+ res.on('end', () => {
156
+ const raw = Buffer.concat(chunks).toString('utf8');
157
+ const durationMs = Date.now() - startMs;
158
+ logger.debug({
159
+ method,
160
+ path: parsedUrl.pathname,
161
+ status: res.statusCode,
162
+ durationMs,
163
+ responseBytes: raw.length
164
+ }, 'http response');
165
+ try {
166
+ resolve({ status: res.statusCode, headers: res.headers, data: JSON.parse(raw) });
167
+ } catch {
168
+ reject(new Error(`Non-JSON response (${res.statusCode}): ${raw.slice(0, 200)}`));
169
+ }
170
+ });
171
+ });
172
+
173
+ req.on('error', (err) => {
174
+ logger.debug({ method, path: parsedUrl.pathname, err }, 'http error');
175
+ reject(err);
176
+ });
177
+ req.setTimeout(timeoutMs, () => {
178
+ req.destroy();
179
+ const seconds = Math.round(timeoutMs / 1000);
180
+ reject(new Error(`Request timeout (${seconds}s)`));
181
+ });
182
+
183
+ if (body) req.write(JSON.stringify(body));
184
+ req.end();
185
+ });
186
+ }
187
+
188
+ module.exports = { request, ApiError, throwApiError, throwNetworkError };