voxflow 1.15.0 → 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/bin/voxflow.js +27 -0
- package/dist/index.js +1 -1
- package/dist/remotion-bundle/02a2fb2eb80bc7bf.woff2 +0 -0
- package/dist/remotion-bundle/052ca5351e5e06ba.woff2 +0 -0
- package/dist/remotion-bundle/05853dd28f4019cb.woff2 +0 -0
- package/dist/remotion-bundle/072ead3737f7c0d0.woff2 +0 -0
- package/dist/remotion-bundle/07d4248613c86a2e.woff2 +0 -0
- package/dist/remotion-bundle/0884a5c2d1d2d99b.woff2 +0 -0
- package/dist/remotion-bundle/0b0e185b2752095e.woff2 +0 -0
- package/dist/remotion-bundle/0e66c11bde067d91.woff2 +0 -0
- package/dist/remotion-bundle/0f7794cfba2c5d21.woff2 +0 -0
- package/dist/remotion-bundle/0fdbae5a4365783a.woff2 +0 -0
- package/dist/remotion-bundle/112.bundle.js +11 -0
- package/dist/remotion-bundle/112.bundle.js.map +1 -0
- package/dist/remotion-bundle/113.bundle.js +11 -0
- package/dist/remotion-bundle/113.bundle.js.map +1 -0
- package/dist/remotion-bundle/119cae0c4c16f7ed.woff2 +0 -0
- package/dist/remotion-bundle/14725f649fd1e78c.woff2 +0 -0
- package/dist/remotion-bundle/14abe9e3f95f7888.woff2 +0 -0
- package/dist/remotion-bundle/163.bundle.js +14678 -0
- package/dist/remotion-bundle/163.bundle.js.map +1 -0
- package/dist/remotion-bundle/1808c54072bf6d14.woff2 +0 -0
- package/dist/remotion-bundle/18948bec3e3012fe.woff2 +0 -0
- package/dist/remotion-bundle/1a661c60d0fc84fc.woff2 +0 -0
- package/dist/remotion-bundle/1af94941e1bc7e1e.woff2 +0 -0
- package/dist/remotion-bundle/1bee0219595f606c.woff2 +0 -0
- package/dist/remotion-bundle/1bfd5da7ce9d4ec4.woff2 +0 -0
- package/dist/remotion-bundle/1c158d56f1884f3f.woff2 +0 -0
- package/dist/remotion-bundle/1cf5e88e667610eb.woff2 +0 -0
- package/dist/remotion-bundle/1d431bd10f53c481.woff2 +0 -0
- package/dist/remotion-bundle/1d701a81a7670db2.woff2 +0 -0
- package/dist/remotion-bundle/1da0fecad4240f16.woff2 +0 -0
- package/dist/remotion-bundle/1ed14d3d0c5c63fe.woff2 +0 -0
- package/dist/remotion-bundle/1edfecf40e586f53.woff2 +0 -0
- package/dist/remotion-bundle/1f479711bc34b054.woff +0 -0
- package/dist/remotion-bundle/1f86e54a0ff5fcd1.woff2 +0 -0
- package/dist/remotion-bundle/2043ea87d9aabd11.woff2 +0 -0
- package/dist/remotion-bundle/20563c39ee8a0e40.woff2 +0 -0
- package/dist/remotion-bundle/20c231590fd12c44.woff2 +0 -0
- package/dist/remotion-bundle/20ce61713f754c07.woff2 +0 -0
- package/dist/remotion-bundle/21eb9306fce24bb1.woff2 +0 -0
- package/dist/remotion-bundle/244bf71c0cc851af.woff2 +0 -0
- package/dist/remotion-bundle/274d4cfc02bffbcb.woff2 +0 -0
- package/dist/remotion-bundle/275.bundle.js +21 -0
- package/dist/remotion-bundle/275.bundle.js.map +1 -0
- package/dist/remotion-bundle/2958f540b39513dc.woff2 +0 -0
- package/dist/remotion-bundle/2a168b98fd97722e.woff2 +0 -0
- package/dist/remotion-bundle/2d1f6373937ab55f.woff2 +0 -0
- package/dist/remotion-bundle/2d213ae47ff6daa9.woff2 +0 -0
- package/dist/remotion-bundle/2e4b1f04fcd05047.woff2 +0 -0
- package/dist/remotion-bundle/304170d98f4c4563.woff2 +0 -0
- package/dist/remotion-bundle/30d02e136e7a5642.woff2 +0 -0
- package/dist/remotion-bundle/3135562b52a714cd.woff2 +0 -0
- package/dist/remotion-bundle/313713af2c8144e9.woff2 +0 -0
- package/dist/remotion-bundle/325fa4108d2285b9.woff2 +0 -0
- package/dist/remotion-bundle/338e927ed3345e0c.woff2 +0 -0
- package/dist/remotion-bundle/35fc6b190365bc17.woff2 +0 -0
- package/dist/remotion-bundle/37a51f1122d4efc5.woff2 +0 -0
- package/dist/remotion-bundle/39a4d63e02736f5e.woff2 +0 -0
- package/dist/remotion-bundle/3a00e0d62dfc4171.woff2 +0 -0
- package/dist/remotion-bundle/3a6955e6561affe1.woff2 +0 -0
- package/dist/remotion-bundle/3c573945aef49b89.woff2 +0 -0
- package/dist/remotion-bundle/3cdbfbfa23b516a5.woff2 +0 -0
- package/dist/remotion-bundle/3e42f85a9e64ca8a.woff2 +0 -0
- package/dist/remotion-bundle/3e83eaf1ec859415.woff2 +0 -0
- package/dist/remotion-bundle/3f3c8c90de1250ee.woff2 +0 -0
- package/dist/remotion-bundle/434.bundle.js +205 -0
- package/dist/remotion-bundle/434.bundle.js.map +1 -0
- package/dist/remotion-bundle/44ffc6ca4d781692.woff2 +0 -0
- package/dist/remotion-bundle/4670d9c4580b09eb.woff2 +0 -0
- package/dist/remotion-bundle/479756881b302824.woff2 +0 -0
- package/dist/remotion-bundle/481b82134bfa9c82.woff2 +0 -0
- package/dist/remotion-bundle/48d27029626f4328.woff2 +0 -0
- package/dist/remotion-bundle/49b7b2a30329c511.woff2 +0 -0
- package/dist/remotion-bundle/4c8b25a1a9337045.woff2 +0 -0
- package/dist/remotion-bundle/4cba14788ca9259b.woff2 +0 -0
- package/dist/remotion-bundle/4cd6c589c004a6a7.woff2 +0 -0
- package/dist/remotion-bundle/4cd8d79c1021608d.woff2 +0 -0
- package/dist/remotion-bundle/4d8fa99b3f00f9f0.woff2 +0 -0
- package/dist/remotion-bundle/4e7805a643f86d53.woff2 +0 -0
- package/dist/remotion-bundle/4ff91be454542e3f.woff2 +0 -0
- package/dist/remotion-bundle/504cbcba1f63591b.woff2 +0 -0
- package/dist/remotion-bundle/5202d792e5791d6c.woff2 +0 -0
- package/dist/remotion-bundle/534db5ad4770cc1d.woff2 +0 -0
- package/dist/remotion-bundle/53b9568eb85f866b.woff2 +0 -0
- package/dist/remotion-bundle/543ad386ca171de9.woff2 +0 -0
- package/dist/remotion-bundle/54798e55bbf7976e.woff2 +0 -0
- package/dist/remotion-bundle/580.bundle.js +11 -0
- package/dist/remotion-bundle/580.bundle.js.map +1 -0
- package/dist/remotion-bundle/58d174d1193af6d1.woff2 +0 -0
- package/dist/remotion-bundle/591d29ff3ff53c80.woff2 +0 -0
- package/dist/remotion-bundle/5c28c4f4824383c6.woff2 +0 -0
- package/dist/remotion-bundle/5da9740d2ce894c8.woff2 +0 -0
- package/dist/remotion-bundle/6197735364642360.woff2 +0 -0
- package/dist/remotion-bundle/6265a4335724080f.woff2 +0 -0
- package/dist/remotion-bundle/633f5e4f6394daa7.woff2 +0 -0
- package/dist/remotion-bundle/637d95ace6a69c49.woff2 +0 -0
- package/dist/remotion-bundle/648e04a04dacff8f.woff2 +0 -0
- package/dist/remotion-bundle/64a6e83045a008b2.woff2 +0 -0
- package/dist/remotion-bundle/651.bundle.js +11 -0
- package/dist/remotion-bundle/651.bundle.js.map +1 -0
- package/dist/remotion-bundle/65e2a988c070facc.woff2 +0 -0
- package/dist/remotion-bundle/66a2f6ce5cc69105.woff2 +0 -0
- package/dist/remotion-bundle/690.bundle.js +3479 -0
- package/dist/remotion-bundle/690.bundle.js.map +1 -0
- package/dist/remotion-bundle/690ff55252ca715d.woff2 +0 -0
- package/dist/remotion-bundle/6a01a1cff49314fc.woff2 +0 -0
- package/dist/remotion-bundle/6cbc32670982986c.woff2 +0 -0
- package/dist/remotion-bundle/6d3cc42ae547f454.woff2 +0 -0
- package/dist/remotion-bundle/6d8f4cfa1ddc0830.woff2 +0 -0
- package/dist/remotion-bundle/6e4d7c6ae65e2dc3.woff2 +0 -0
- package/dist/remotion-bundle/6e86418bbcefb2e8.woff2 +0 -0
- package/dist/remotion-bundle/6ee02884b29cf7fb.woff2 +0 -0
- package/dist/remotion-bundle/6f436a74c9e3252c.woff2 +0 -0
- package/dist/remotion-bundle/78c8022f1657618b.woff2 +0 -0
- package/dist/remotion-bundle/7c5444169792bca4.woff2 +0 -0
- package/dist/remotion-bundle/7c86bddd9d997212.woff2 +0 -0
- package/dist/remotion-bundle/7e1284684767f584.woff2 +0 -0
- package/dist/remotion-bundle/7e81c17522d182b2.woff2 +0 -0
- package/dist/remotion-bundle/7eb87be198f7858c.woff2 +0 -0
- package/dist/remotion-bundle/8060c928f948aab5.woff2 +0 -0
- package/dist/remotion-bundle/80bc9dfbea2b35ae.woff2 +0 -0
- package/dist/remotion-bundle/811b83f69963bb48.woff2 +0 -0
- package/dist/remotion-bundle/813.bundle.js +117511 -0
- package/dist/remotion-bundle/813.bundle.js.map +1 -0
- package/dist/remotion-bundle/84df492e349f82e9.woff2 +0 -0
- package/dist/remotion-bundle/8501bfd73eb36f2b.woff2 +0 -0
- package/dist/remotion-bundle/854236a8376093fe.woff2 +0 -0
- package/dist/remotion-bundle/8571d74529082753.woff2 +0 -0
- package/dist/remotion-bundle/860bf44f8e6f4b5d.woff2 +0 -0
- package/dist/remotion-bundle/879.bundle.js +64 -0
- package/dist/remotion-bundle/879.bundle.js.map +1 -0
- package/dist/remotion-bundle/887dd482f848d56f.woff2 +0 -0
- package/dist/remotion-bundle/89b2132e85fbbb5a.woff2 +0 -0
- package/dist/remotion-bundle/8ba60d6c306010c2.woff2 +0 -0
- package/dist/remotion-bundle/8c7c4dadea897806.woff2 +0 -0
- package/dist/remotion-bundle/8c943f9999706f61.woff2 +0 -0
- package/dist/remotion-bundle/8f2a718c90575cc9.woff2 +0 -0
- package/dist/remotion-bundle/906b6edb3e1772c9.woff2 +0 -0
- package/dist/remotion-bundle/930ff9daccdf14eb.woff2 +0 -0
- package/dist/remotion-bundle/934db2f1c403c4d0.woff2 +0 -0
- package/dist/remotion-bundle/938.bundle.js +451 -0
- package/dist/remotion-bundle/938.bundle.js.map +1 -0
- package/dist/remotion-bundle/967.bundle.js +4462 -0
- package/dist/remotion-bundle/967.bundle.js.map +1 -0
- package/dist/remotion-bundle/9684a1093d3c02ce.woff2 +0 -0
- package/dist/remotion-bundle/973dcd0faa6116cc.woff2 +0 -0
- package/dist/remotion-bundle/9745400694e76cd8.woff2 +0 -0
- package/dist/remotion-bundle/999ef957bed3bdca.woff2 +0 -0
- package/dist/remotion-bundle/99a3d67c8b0f43e3.woff2 +0 -0
- package/dist/remotion-bundle/a0586c3e03127283.woff2 +0 -0
- package/dist/remotion-bundle/a0eb654fdae46269.woff2 +0 -0
- package/dist/remotion-bundle/a20e35d3b08f7994.woff2 +0 -0
- package/dist/remotion-bundle/a2dcaced7c8c25ab.woff2 +0 -0
- package/dist/remotion-bundle/a79255a972a2681a.woff2 +0 -0
- package/dist/remotion-bundle/a804b352cb9fec1a.woff2 +0 -0
- package/dist/remotion-bundle/aae7117164e1eabc.woff2 +0 -0
- package/dist/remotion-bundle/affd121385d0442d.woff2 +0 -0
- package/dist/remotion-bundle/b19a6083987ee0d7.woff2 +0 -0
- package/dist/remotion-bundle/b1b2bd04d8637981.woff2 +0 -0
- package/dist/remotion-bundle/b2c07f341486be87.woff2 +0 -0
- package/dist/remotion-bundle/b33d8f82e575c4ce.woff2 +0 -0
- package/dist/remotion-bundle/b366c0bed35ef491.woff2 +0 -0
- package/dist/remotion-bundle/b41e857ec1b85642.woff2 +0 -0
- package/dist/remotion-bundle/b420bb34ccf23e7f.woff2 +0 -0
- package/dist/remotion-bundle/b4f7bf4efb0c0ccf.woff2 +0 -0
- package/dist/remotion-bundle/b60fe5eca03cff93.woff2 +0 -0
- package/dist/remotion-bundle/b6bd31a336e64bce.woff2 +0 -0
- package/dist/remotion-bundle/b6d2befba3dfefeb.woff2 +0 -0
- package/dist/remotion-bundle/b75f39ab06c43bf4.woff2 +0 -0
- package/dist/remotion-bundle/b77880e8c413d4fd.woff2 +0 -0
- package/dist/remotion-bundle/b7e38ec441e4a77a.woff2 +0 -0
- package/dist/remotion-bundle/b83baa383ff0bf2b.woff2 +0 -0
- package/dist/remotion-bundle/b9ad7b6c0a11450a.woff2 +0 -0
- package/dist/remotion-bundle/baf84486e8ae3aaf.woff2 +0 -0
- package/dist/remotion-bundle/bc047b1f6869cffa.woff2 +0 -0
- package/dist/remotion-bundle/bf4f3ac6e93f33aa.woff2 +0 -0
- package/dist/remotion-bundle/bf6835ffec5897a2.woff2 +0 -0
- package/dist/remotion-bundle/bf8885f581eb1724.woff2 +0 -0
- package/dist/remotion-bundle/bundle.js +83376 -0
- package/dist/remotion-bundle/bundle.js.map +1 -0
- package/dist/remotion-bundle/c03f046bccd789d0.woff2 +0 -0
- package/dist/remotion-bundle/c0bb1f8962b73bc3.woff2 +0 -0
- package/dist/remotion-bundle/c1003f9a7db6e1cf.woff2 +0 -0
- package/dist/remotion-bundle/c15d83fb1e199515.woff2 +0 -0
- package/dist/remotion-bundle/c28e7e5d310f73ef.woff2 +0 -0
- package/dist/remotion-bundle/c2b840274db78aea.woff2 +0 -0
- package/dist/remotion-bundle/c3000e3299d4e45f.woff2 +0 -0
- package/dist/remotion-bundle/c83ce886e5288510.woff2 +0 -0
- package/dist/remotion-bundle/c87a5a64d4ac0918.woff2 +0 -0
- package/dist/remotion-bundle/c8a7e0d049e965fa.woff2 +0 -0
- package/dist/remotion-bundle/c949a35d3a3b1faf.woff2 +0 -0
- package/dist/remotion-bundle/c9618c9b9ac2bc78.woff2 +0 -0
- package/dist/remotion-bundle/ca3add3b84152d5b.woff2 +0 -0
- package/dist/remotion-bundle/cad9dd036408d707.woff2 +0 -0
- package/dist/remotion-bundle/cbb24916619df439.woff2 +0 -0
- package/dist/remotion-bundle/cc054f0b5514e177.woff2 +0 -0
- package/dist/remotion-bundle/ccc248ed9312bc71.woff2 +0 -0
- package/dist/remotion-bundle/cd9d623aa07af925.woff2 +0 -0
- package/dist/remotion-bundle/ce2ba7a321bd1247.woff2 +0 -0
- package/dist/remotion-bundle/cf72455f79a29b14.woff2 +0 -0
- package/dist/remotion-bundle/d267cbfefab452ac.woff2 +0 -0
- package/dist/remotion-bundle/d435cff46a64955f.woff +0 -0
- package/dist/remotion-bundle/d494d07f67e363f6.woff2 +0 -0
- package/dist/remotion-bundle/d7aa0cc1fa47bf38.woff2 +0 -0
- package/dist/remotion-bundle/d7c5ca93d885160a.woff2 +0 -0
- package/dist/remotion-bundle/d855d3e252db74e2.woff2 +0 -0
- package/dist/remotion-bundle/d8f13d47f02f82c2.woff2 +0 -0
- package/dist/remotion-bundle/d9567cce2ee11019.woff2 +0 -0
- package/dist/remotion-bundle/db8d4456fc75dd86.woff +0 -0
- package/dist/remotion-bundle/dc274628378c47ee.woff2 +0 -0
- package/dist/remotion-bundle/dc3e06947bb69903.woff2 +0 -0
- package/dist/remotion-bundle/dd67040ac3b6d523.woff2 +0 -0
- package/dist/remotion-bundle/e0b04bd488f953f4.woff2 +0 -0
- package/dist/remotion-bundle/e2a572ff95089370.woff2 +0 -0
- package/dist/remotion-bundle/e2e18a86b1c2b0cc.woff2 +0 -0
- package/dist/remotion-bundle/e3a78ee2fc9c6931.woff2 +0 -0
- package/dist/remotion-bundle/e654c9d547605a9f.woff2 +0 -0
- package/dist/remotion-bundle/e67a3a64c129927c.woff2 +0 -0
- package/dist/remotion-bundle/e6be28b4203cd6ce.woff2 +0 -0
- package/dist/remotion-bundle/e841907ad9b0a191.woff +0 -0
- package/dist/remotion-bundle/e889d1541c69fffa.woff2 +0 -0
- package/dist/remotion-bundle/e88ef8c76373a9e2.woff2 +0 -0
- package/dist/remotion-bundle/e9c72f4bc37defef.woff2 +0 -0
- package/dist/remotion-bundle/e9e35f863403a255.woff2 +0 -0
- package/dist/remotion-bundle/eb23b37b009375da.woff2 +0 -0
- package/dist/remotion-bundle/ee1342b741625721.woff2 +0 -0
- package/dist/remotion-bundle/f07da88543a57ec9.woff2 +0 -0
- package/dist/remotion-bundle/f522982115306f8a.woff2 +0 -0
- package/dist/remotion-bundle/f8449bd864e6d8bc.woff2 +0 -0
- package/dist/remotion-bundle/f906dd5bd95ff9ab.woff2 +0 -0
- package/dist/remotion-bundle/f9e9e9413e3c38bb.woff2 +0 -0
- package/dist/remotion-bundle/fa5a5b16280994a8.woff2 +0 -0
- package/dist/remotion-bundle/favicon.ico +0 -0
- package/dist/remotion-bundle/fb19c0517725599b.woff2 +0 -0
- package/dist/remotion-bundle/fcaf24232f684b9b.woff2 +0 -0
- package/dist/remotion-bundle/fe09e084a3eea8cf.woff2 +0 -0
- package/dist/remotion-bundle/ff38d5317df7345a.woff2 +0 -0
- package/dist/remotion-bundle/ffe7ea1ea08f455a.woff2 +0 -0
- package/dist/remotion-bundle/index.html +49 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaomei/communication/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoxin/career/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/female-kefu-xiaoyue/parenting/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/male-kefu-xiaoxu/time-trap/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/cognition/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/growth/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/parenting/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-A6b7WpG3/soothing/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-female-R2s4N9qJ/cognition/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-Bk7vD3xP/decision/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-W1tH9jVc/manager/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide/v-male-s5NqE0rZ/founder/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/career-advice/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/founder-lesson/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/incident-review/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/learning-loop/4.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/meeting-closure/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/product-update/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/product-update/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/product-update/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/product-update/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/research-reading/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/0.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/1.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/2.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/3.mp3 +0 -0
- package/dist/remotion-bundle/public/paper-slide-experiments/sales-enablement/4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-0.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-1.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-2.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-3.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/ai-life/card-5.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-0.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-1.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-2.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-3.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-5.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/coffee-science/card-6.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-0.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-1.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-2.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-3.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-5.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/reading-secrets/card-6.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-0.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-1.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-2.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-3.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-4.mp3 +0 -0
- package/dist/remotion-bundle/public/voiceover/remote-work/card-5.mp3 +0 -0
- package/dist/remotion-bundle/source-map-helper.wasm +0 -0
- package/lib/cli.js +270 -0
- package/lib/commands/_registry.js +48 -0
- package/lib/commands/add.js +242 -0
- package/lib/commands/asr/azure-transcribe.js +336 -0
- package/lib/commands/asr/cloud-transcribe.js +384 -0
- package/lib/commands/asr/helpers.js +76 -0
- package/lib/commands/asr/index.js +236 -0
- package/lib/commands/asr/local-transcribe.js +125 -0
- package/lib/commands/asr-jobs.js +257 -0
- package/lib/commands/asr.js +11 -0
- package/lib/commands/auth-cmds.js +358 -0
- package/lib/commands/dub.js +542 -0
- package/lib/commands/explain.js +512 -0
- package/lib/commands/feedback.js +152 -0
- package/lib/commands/image.js +207 -0
- package/lib/commands/mcp-key.js +166 -0
- package/lib/commands/narrate.js +639 -0
- package/lib/commands/picstory-templates.js +276 -0
- package/lib/commands/picstory.js +547 -0
- package/lib/commands/podcast/dialogue.js +109 -0
- package/lib/commands/podcast/generate.js +127 -0
- package/lib/commands/podcast/index.js +561 -0
- package/lib/commands/podcast/synthesize.js +188 -0
- package/lib/commands/podcast.js +11 -0
- package/lib/commands/present.js +519 -0
- package/lib/commands/publish.js +415 -0
- package/lib/commands/skills.js +473 -0
- package/lib/commands/slice-render.js +282 -0
- package/lib/commands/slice-stage.js +264 -0
- package/lib/commands/slice.js +346 -0
- package/lib/commands/slides/constants.js +108 -0
- package/lib/commands/slides/html-renderer.js +338 -0
- package/lib/commands/slides/index.js +345 -0
- package/lib/commands/slides.js +11 -0
- package/lib/commands/story.js +302 -0
- package/lib/commands/summarize.js +532 -0
- package/lib/commands/synthesize.js +261 -0
- package/lib/commands/translate.js +593 -0
- package/lib/commands/upgrade.js +249 -0
- package/lib/commands/video-translate.js +577 -0
- package/lib/commands/voices.js +292 -0
- package/lib/core/agent-env.js +104 -0
- package/lib/core/args.js +107 -0
- package/lib/core/asr-client.js +448 -0
- package/lib/core/asr-jobs-client.js +126 -0
- package/lib/core/asr-jobs-store.js +105 -0
- package/lib/core/asr-r2-upload.js +181 -0
- package/lib/core/asr-upload.js +132 -0
- package/lib/core/audio-extract.js +150 -0
- package/lib/core/audio.js +219 -0
- package/lib/core/auth.js +880 -0
- package/lib/core/config.js +197 -0
- package/lib/core/feedback.js +64 -0
- package/lib/core/ffmpeg.js +476 -0
- package/lib/core/http.js +188 -0
- package/lib/core/image-client.js +55 -0
- package/lib/core/intent-params.js +11 -0
- package/lib/core/llm-client.js +76 -0
- package/lib/core/logger.js +208 -0
- package/lib/core/mic-recorder.js +182 -0
- package/lib/core/pause-markers.js +94 -0
- package/lib/core/podcast-pacing.js +118 -0
- package/lib/core/spinner.js +33 -0
- package/lib/core/srt.js +394 -0
- package/lib/core/telemetry.js +100 -0
- package/lib/core/timeline.js +92 -0
- package/lib/core/tts-synthesizer.js +70 -0
- package/lib/core/update-check.js +185 -0
- package/lib/core/url-download.js +148 -0
- package/lib/core/whisper-local.js +279 -0
- package/lib/internal/deck-validator.js +488 -0
- package/lib/internal/slice-themes.json +370 -0
- package/lib/stage-core/cloud-render.js +170 -0
- package/lib/stage-core/deck-format.js +133 -0
- package/lib/stage-core/edit-prompt.js +104 -0
- package/lib/stage-core/event-bus.js +31 -0
- package/lib/stage-core/port.js +46 -0
- package/lib/stage-core/server.js +352 -0
- package/lib/stage-core/snapshot-store.js +198 -0
- package/lib/stage-core/watcher.js +106 -0
- package/lib/stage-ui/slice/template.js +1672 -0
- package/package.json +9 -4
- package/skills/.claude-plugin/marketplace.json +22 -0
- package/skills/.claude-plugin/plugin.json +25 -0
- package/skills/LICENSE +21 -0
- package/skills/README.md +120 -0
- package/skills/hub/SKILL.md +317 -0
- package/skills/podcast/SKILL.md +146 -0
- package/skills/slice/SKILL.md +205 -0
- package/skills/slice/agents/openai.yaml +4 -0
- package/skills/slice/references/deck-schema.md +183 -0
- package/skills/slice/references/example-decks.md +108 -0
- package/skills/slice/references/themes.md +172 -0
- package/skills/transcribe/SKILL.md +473 -0
- package/skills/video/SKILL.md +261 -0
- package/skills/voxflow-slice/SKILL.md +271 -0
- package/skills/voxflow-slice/examples/article.md +13 -0
- package/skills/voxflow-slice/examples/expected-deck.json +39 -0
- package/skills/voxflow-slice/examples/validate.mjs +46 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoxFlow CLI — Narrate command
|
|
3
|
+
*
|
|
4
|
+
* Convert file, text, or script to multi-segment TTS audio.
|
|
5
|
+
* Three input modes:
|
|
6
|
+
* 1. Plain text: --text "..." or stdin pipe
|
|
7
|
+
* 2. File: --input file.txt / file.md
|
|
8
|
+
* 3. Script: --script script.json (per-segment voice/speed control)
|
|
9
|
+
*
|
|
10
|
+
* Supports output formats: WAV (default), MP3.
|
|
11
|
+
* Pipeline: read input → split segments → TTS each → concatenate → output
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { NARRATE_DEFAULTS } = require('../core/config');
|
|
17
|
+
const { ApiError } = require('../core/http');
|
|
18
|
+
const { parseParagraphs, buildWav, concatAudioBuffers, getFileExtension } = require('../core/audio');
|
|
19
|
+
const { synthesizeTTS } = require('../core/tts-synthesizer');
|
|
20
|
+
|
|
21
|
+
// ─── Script Parsing ──────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse and validate a script JSON file.
|
|
25
|
+
* @param {string} filePath - Path to script.json
|
|
26
|
+
* @returns {{ segments: Array<{text: string, voiceId?: string, speed?: number, volume?: number, pitch?: number}>, silence: number, output?: string }}
|
|
27
|
+
*/
|
|
28
|
+
function parseScript(filePath) {
|
|
29
|
+
if (!fs.existsSync(filePath)) {
|
|
30
|
+
throw new Error(`Script file not found: ${filePath}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let raw;
|
|
34
|
+
try {
|
|
35
|
+
raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
36
|
+
} catch (err) {
|
|
37
|
+
throw new Error(`Invalid JSON in script file: ${err.message}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!raw.segments || !Array.isArray(raw.segments) || raw.segments.length === 0) {
|
|
41
|
+
throw new Error('Script must have a non-empty "segments" array');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < raw.segments.length; i++) {
|
|
45
|
+
const seg = raw.segments[i];
|
|
46
|
+
if (!seg.text || typeof seg.text !== 'string' || seg.text.trim().length === 0) {
|
|
47
|
+
throw new Error(`Segment ${i + 1} must have a non-empty "text" field`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
segments: raw.segments.map((s) => ({
|
|
53
|
+
text: s.text.trim(),
|
|
54
|
+
voiceId: s.voiceId || undefined,
|
|
55
|
+
speed: s.speed != null ? Number(s.speed) : undefined,
|
|
56
|
+
volume: s.volume != null ? Number(s.volume) : undefined,
|
|
57
|
+
pitch: s.pitch != null ? Number(s.pitch) : undefined,
|
|
58
|
+
})),
|
|
59
|
+
silence: raw.silence != null ? Number(raw.silence) : NARRATE_DEFAULTS.silence,
|
|
60
|
+
output: raw.output || undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Markdown Stripping ──────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Strip basic markdown syntax for cleaner TTS input.
|
|
68
|
+
* Removes: headings (#), bold (**), italic (*), links [text](url), images, code blocks.
|
|
69
|
+
*/
|
|
70
|
+
function stripMarkdown(text) {
|
|
71
|
+
return text
|
|
72
|
+
// Remove code blocks (``` ... ```)
|
|
73
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
74
|
+
// Remove inline code (`...`)
|
|
75
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
76
|
+
// Remove images 
|
|
77
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
|
78
|
+
// Convert links [text](url) → text
|
|
79
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
80
|
+
// Remove headings (# ... )
|
|
81
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
82
|
+
// Remove bold/italic markers
|
|
83
|
+
.replace(/\*{1,3}([^*]+)\*{1,3}/g, '$1')
|
|
84
|
+
.replace(/_{1,3}([^_]+)_{1,3}/g, '$1')
|
|
85
|
+
// Remove horizontal rules
|
|
86
|
+
.replace(/^[-*_]{3,}\s*$/gm, '')
|
|
87
|
+
// Remove blockquote markers
|
|
88
|
+
.replace(/^>\s?/gm, '')
|
|
89
|
+
// Clean up extra blank lines
|
|
90
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
91
|
+
.trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Stdin Reading ───────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read all data from stdin (for pipe mode).
|
|
98
|
+
* @returns {Promise<string>}
|
|
99
|
+
*/
|
|
100
|
+
async function readStdin() {
|
|
101
|
+
const chunks = [];
|
|
102
|
+
for await (const chunk of process.stdin) {
|
|
103
|
+
chunks.push(chunk);
|
|
104
|
+
}
|
|
105
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── TTS Synthesis ───────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
async function synthesizeSegment(apiBase, token, text, voiceId, speed, volume, pitch, format, index, total) {
|
|
111
|
+
const result = await synthesizeTTS({
|
|
112
|
+
apiBase, token, text, voiceId,
|
|
113
|
+
speed: speed ?? 1.0,
|
|
114
|
+
volume: volume ?? 1.0,
|
|
115
|
+
pitch,
|
|
116
|
+
format: format || 'pcm',
|
|
117
|
+
index, total,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const formatLabel = format === 'mp3' ? 'MP3' : format === 'wav' ? 'WAV' : 'PCM';
|
|
121
|
+
console.log(` OK (${(result.audio.length / 1024).toFixed(0)} KB ${formatLabel})`);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Risk scanner (pre-flight) ───────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Heuristic check for segments likely to trigger TTS provider edge cases.
|
|
129
|
+
* Most prominent: Issue #2901 — Chinese TTS voices return tts_failed on
|
|
130
|
+
* isolated short Latin fragments (single brand names, short English headings).
|
|
131
|
+
*
|
|
132
|
+
* Returns an array of { index, severity, message, text }. Empty array when
|
|
133
|
+
* no risks are detected. Does NOT block — purely informational.
|
|
134
|
+
*/
|
|
135
|
+
function scanSegmentRisks(segments) {
|
|
136
|
+
const risks = [];
|
|
137
|
+
for (let i = 0; i < segments.length; i++) {
|
|
138
|
+
const text = (segments[i].text || '').trim();
|
|
139
|
+
const isShort = text.length > 0 && text.length < 30;
|
|
140
|
+
const isLatinOnly = /^[A-Za-z0-9 .,_\-:;'"!?@#$%&*()/]+$/.test(text);
|
|
141
|
+
const hasLatinWord = /[A-Za-z]{2,}/.test(text);
|
|
142
|
+
|
|
143
|
+
if (isShort && isLatinOnly) {
|
|
144
|
+
risks.push({
|
|
145
|
+
index: i + 1,
|
|
146
|
+
severity: 'high',
|
|
147
|
+
text,
|
|
148
|
+
message: `pure-Latin short line (${text.length} chars) — Chinese TTS often returns tts_failed (Issue #2901). Try padding with Chinese context.`,
|
|
149
|
+
});
|
|
150
|
+
} else if (text.length < 25 && hasLatinWord) {
|
|
151
|
+
// Mixed CJK + Latin short text. Empirically the threshold below which
|
|
152
|
+
// a Chinese voice on a Chinese-mode call still fails on the embedded
|
|
153
|
+
// Latin words — saw 17-char "测试 VoxFlow 的 narrate 命令" trigger it.
|
|
154
|
+
risks.push({
|
|
155
|
+
index: i + 1,
|
|
156
|
+
severity: 'medium',
|
|
157
|
+
text,
|
|
158
|
+
message: `short line (${text.length} chars) with Latin word(s) — may fail; CLI will auto-retry with Chinese context wrap.`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return risks;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Determine whether a segment is a candidate for the #2901 auto-wrap retry.
|
|
167
|
+
* We only retry when the failure is plausibly a Chinese-TTS-vs-Latin issue
|
|
168
|
+
* AND the text is short enough that wrapping with Chinese context is sane.
|
|
169
|
+
*/
|
|
170
|
+
function shouldAutoWrap(text) {
|
|
171
|
+
const t = (text || '').trim();
|
|
172
|
+
if (!t || t.length > 60) return false;
|
|
173
|
+
// Latin-only OR latin-heavy short text where adding Chinese context
|
|
174
|
+
// empirically rescues the synthesis.
|
|
175
|
+
const isLatinOnly = /^[A-Za-z0-9 .,_\-:;'"!?@#$%&*()/]+$/.test(t);
|
|
176
|
+
const latinChars = (t.match(/[A-Za-z]/g) || []).length;
|
|
177
|
+
return isLatinOnly || (latinChars / t.length > 0.4 && t.length < 30);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Wrap pseudonym: prefix with a short Chinese phrase that gives the TTS
|
|
182
|
+
* model enough Chinese context to switch into Mandarin mode for the trailing
|
|
183
|
+
* Latin tokens. Empirically, "如下:" works well; ending with "。" caps the
|
|
184
|
+
* sentence so prosody settles.
|
|
185
|
+
*/
|
|
186
|
+
function chineseContextWrap(text) {
|
|
187
|
+
return `如下:${text}。`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Detect a TTS-provider rejection from an ApiError. The CLI's `throwApiError`
|
|
192
|
+
* wraps any 5xx into `code='server_error'` and embeds the backend's own code
|
|
193
|
+
* (e.g. `tts_failed`) into the message in the `[tts_failed]` form. We pattern
|
|
194
|
+
* match on that marker to distinguish #2901-style provider rejections from
|
|
195
|
+
* real server faults (which we shouldn't auto-retry — that just amplifies
|
|
196
|
+
* load on a struggling backend).
|
|
197
|
+
*/
|
|
198
|
+
function isTtsProviderRejection(err) {
|
|
199
|
+
if (!(err instanceof ApiError)) return false;
|
|
200
|
+
if (err.code === 'tts_failed') return true; // future-proof: if propagated cleanly
|
|
201
|
+
return /\[tts_failed\]/.test(err.message || '');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Synthesize one segment with one auto-retry on `tts_failed` using a
|
|
206
|
+
* Chinese-context wrap. Returns the API result on success; the audio belongs
|
|
207
|
+
* to the wrapped text, but the user-visible transcript still shows the
|
|
208
|
+
* original — that's intentional, the wrap is silent compensation.
|
|
209
|
+
*
|
|
210
|
+
* Throws every other error class (token_expired, quota_exceeded, network)
|
|
211
|
+
* unchanged so the outer loop can fast-fail.
|
|
212
|
+
*/
|
|
213
|
+
async function synthesizeWithRetry(opts, attempts = { wrap: 0 }) {
|
|
214
|
+
try {
|
|
215
|
+
return await synthesizeSegment(
|
|
216
|
+
opts.apiBase, opts.token, opts.text, opts.voiceId,
|
|
217
|
+
opts.speed, opts.volume, opts.pitch, opts.format,
|
|
218
|
+
opts.index, opts.total,
|
|
219
|
+
);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (isTtsProviderRejection(err) && attempts.wrap === 0 && shouldAutoWrap(opts.text)) {
|
|
222
|
+
const wrapped = chineseContextWrap(opts.text);
|
|
223
|
+
process.stdout.write(`\n ↻ retrying with Chinese context wrap: ${JSON.stringify(wrapped)}\n TTS [${opts.index + 1}/${opts.total}]...`);
|
|
224
|
+
return await synthesizeWithRetry({ ...opts, text: wrapped }, { wrap: 1 });
|
|
225
|
+
}
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Narrate text, file, or script to a WAV file.
|
|
234
|
+
*
|
|
235
|
+
* @param {object} opts
|
|
236
|
+
* @param {string} opts.token - JWT token
|
|
237
|
+
* @param {string} opts.api - API base URL
|
|
238
|
+
* @param {string} [opts.text] - Inline text to narrate
|
|
239
|
+
* @param {string} [opts.input] - Path to .txt or .md file
|
|
240
|
+
* @param {string} [opts.script] - Path to .json script file
|
|
241
|
+
* @param {string} [opts.voice] - Default TTS voice ID
|
|
242
|
+
* @param {number} [opts.speed] - Default TTS speed
|
|
243
|
+
* @param {string} [opts.format] - Output format: pcm, wav, mp3 (default: pcm → WAV)
|
|
244
|
+
* @param {number} [opts.silence] - Silence gap between segments (seconds)
|
|
245
|
+
* @param {string} [opts.output] - Output WAV path
|
|
246
|
+
* @returns {Promise<{outputPath: string, textPath: string, duration: number, quotaUsed: number, segmentCount: number, format: string}>}
|
|
247
|
+
*/
|
|
248
|
+
async function narrate(opts) {
|
|
249
|
+
let isExiting = false;
|
|
250
|
+
|
|
251
|
+
const sigintHandler = () => {
|
|
252
|
+
if (isExiting) return;
|
|
253
|
+
isExiting = true;
|
|
254
|
+
console.log('\n\nNarration cancelled.');
|
|
255
|
+
process.exit(130);
|
|
256
|
+
};
|
|
257
|
+
process.on('SIGINT', sigintHandler);
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
return await _narrate(opts);
|
|
261
|
+
} finally {
|
|
262
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function _narrate(opts) {
|
|
267
|
+
const defaultVoice = opts.voice || NARRATE_DEFAULTS.voice;
|
|
268
|
+
const defaultSpeed = opts.speed ?? NARRATE_DEFAULTS.speed;
|
|
269
|
+
const format = opts.format || 'pcm'; // API format: pcm | wav | mp3
|
|
270
|
+
const api = opts.api;
|
|
271
|
+
const token = opts.token;
|
|
272
|
+
|
|
273
|
+
let segments; // Array of { text, voiceId?, speed?, volume?, pitch? }
|
|
274
|
+
let silence;
|
|
275
|
+
let outputPath;
|
|
276
|
+
let inputLabel;
|
|
277
|
+
|
|
278
|
+
// ─── Determine input source ────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
if (opts.script) {
|
|
281
|
+
// Script mode: JSON with per-segment control
|
|
282
|
+
const script = parseScript(opts.script);
|
|
283
|
+
segments = script.segments;
|
|
284
|
+
silence = opts.silence ?? script.silence;
|
|
285
|
+
outputPath = opts.output || script.output;
|
|
286
|
+
inputLabel = `script: ${opts.script} (${segments.length} segments)`;
|
|
287
|
+
|
|
288
|
+
} else if (opts.input) {
|
|
289
|
+
// File mode: read .txt or .md
|
|
290
|
+
const filePath = path.resolve(opts.input);
|
|
291
|
+
if (!fs.existsSync(filePath)) {
|
|
292
|
+
throw new Error(`Input file not found: ${filePath}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
296
|
+
|
|
297
|
+
// Strip markdown for .md files
|
|
298
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
299
|
+
if (ext === '.md' || ext === '.markdown') {
|
|
300
|
+
content = stripMarkdown(content);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const paragraphs = parseParagraphs(content);
|
|
304
|
+
if (paragraphs.length === 0) {
|
|
305
|
+
throw new Error('No text content found in input file');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
segments = paragraphs.map((text) => ({ text }));
|
|
309
|
+
silence = opts.silence ?? NARRATE_DEFAULTS.silence;
|
|
310
|
+
outputPath = opts.output;
|
|
311
|
+
inputLabel = `file: ${opts.input} (${segments.length} paragraphs)`;
|
|
312
|
+
|
|
313
|
+
} else if (opts.text) {
|
|
314
|
+
// Plain text mode
|
|
315
|
+
const paragraphs = parseParagraphs(opts.text);
|
|
316
|
+
if (paragraphs.length === 0) {
|
|
317
|
+
throw new Error('No text content provided');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
segments = paragraphs.map((text) => ({ text }));
|
|
321
|
+
silence = opts.silence ?? NARRATE_DEFAULTS.silence;
|
|
322
|
+
outputPath = opts.output;
|
|
323
|
+
inputLabel = `text: ${opts.text.length} chars (${segments.length} paragraphs)`;
|
|
324
|
+
|
|
325
|
+
} else if (!process.stdin.isTTY) {
|
|
326
|
+
// Stdin pipe mode
|
|
327
|
+
const stdinText = await readStdin();
|
|
328
|
+
if (!stdinText || stdinText.trim().length === 0) {
|
|
329
|
+
throw new Error('No input provided via stdin');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const paragraphs = parseParagraphs(stdinText);
|
|
333
|
+
if (paragraphs.length === 0) {
|
|
334
|
+
throw new Error('No text content found in stdin input');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
segments = paragraphs.map((text) => ({ text }));
|
|
338
|
+
silence = opts.silence ?? NARRATE_DEFAULTS.silence;
|
|
339
|
+
inputLabel = `stdin (${segments.length} paragraphs)`;
|
|
340
|
+
|
|
341
|
+
} else {
|
|
342
|
+
throw new Error(
|
|
343
|
+
'No input provided. Use one of:\n' +
|
|
344
|
+
' --input <file.txt> Read a text or markdown file\n' +
|
|
345
|
+
' --text "text" Provide inline text\n' +
|
|
346
|
+
' --script <file.json> Use a script with per-segment control\n' +
|
|
347
|
+
' echo "text" | voxflow narrate Pipe from stdin'
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Default output path. Prefer the input file's basename — matches the
|
|
352
|
+
// user's mental model: `voxflow narrate --input design.md` → `design.wav`.
|
|
353
|
+
// Falls back to a timestamped name only when there's no input file
|
|
354
|
+
// (--text mode, --script JSON without explicit output, stdin pipe).
|
|
355
|
+
//
|
|
356
|
+
// Use path.join(process.cwd(), ...) explicitly. path.resolve() should
|
|
357
|
+
// do the same, but in the ncc-bundled dist/index.js it occasionally
|
|
358
|
+
// resolved against the bundle's runtime dir during 1.10.9 verify —
|
|
359
|
+
// output ended up at node_modules/voxflow/dist/cli/<file>. Joining
|
|
360
|
+
// process.cwd() explicitly side-steps that.
|
|
361
|
+
// CRITICAL: ncc's static asset analyzer rewrites
|
|
362
|
+
// path.join(<dynamic>, `${expr}${ext}`)
|
|
363
|
+
// into __webpack_require__.ab + 'cli/' + expr + ext when the template
|
|
364
|
+
// starts with a non-literal — output ended up at dist/cli/<file> in
|
|
365
|
+
// 1.10.10/.11. Build the filename string FIRST so path.join sees two
|
|
366
|
+
// opaque arguments and the analyzer leaves it alone.
|
|
367
|
+
const ext = getFileExtension(format);
|
|
368
|
+
if (!outputPath) {
|
|
369
|
+
let filename;
|
|
370
|
+
if (opts.input) {
|
|
371
|
+
const base = path.basename(opts.input, path.extname(opts.input));
|
|
372
|
+
filename = base + ext;
|
|
373
|
+
} else {
|
|
374
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
375
|
+
filename = 'narration-' + ts + ext;
|
|
376
|
+
}
|
|
377
|
+
outputPath = path.join(process.cwd(), filename);
|
|
378
|
+
}
|
|
379
|
+
if (!outputPath.endsWith(ext)) {
|
|
380
|
+
// Replace wrong extension or append
|
|
381
|
+
outputPath = outputPath.replace(/\.(wav|mp3|pcm)$/i, '') + ext;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Print header
|
|
385
|
+
console.log('\n=== VoxFlow Narrate ===');
|
|
386
|
+
console.log(`Input: ${inputLabel}`);
|
|
387
|
+
console.log(`Voice: ${defaultVoice}${opts.script ? ' (may be overridden per segment)' : ''}`);
|
|
388
|
+
console.log(`Format: ${format === 'pcm' ? 'wav (pcm)' : format}`);
|
|
389
|
+
console.log(`Speed: ${defaultSpeed}`);
|
|
390
|
+
if (format === 'mp3') {
|
|
391
|
+
console.log(`Output: ${outputPath}`);
|
|
392
|
+
console.log(` (MP3 mode: no silence inserted between segments)`);
|
|
393
|
+
} else {
|
|
394
|
+
console.log(`Silence: ${silence}s`);
|
|
395
|
+
console.log(`Output: ${outputPath}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ─── Pre-flight risk scan ──────────────────────────────────────────────
|
|
399
|
+
// Flag segments likely to hit known provider edge cases (e.g. #2901)
|
|
400
|
+
// BEFORE making any API calls. Informational only — does not block.
|
|
401
|
+
|
|
402
|
+
const risks = scanSegmentRisks(segments);
|
|
403
|
+
if (risks.length > 0) {
|
|
404
|
+
console.log('');
|
|
405
|
+
console.log(`⚠ ${risks.length} segment(s) may trigger TTS edge cases:`);
|
|
406
|
+
for (const r of risks) {
|
|
407
|
+
const preview = r.text.length > 50 ? `${r.text.slice(0, 50)}…` : r.text;
|
|
408
|
+
console.log(` [${r.severity}] segment ${r.index}: ${JSON.stringify(preview)}`);
|
|
409
|
+
console.log(` → ${r.message}`);
|
|
410
|
+
}
|
|
411
|
+
console.log(' CLI will auto-retry with Chinese context wrap on tts_failed.');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ─── TTS synthesis loop ────────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
console.log(`\n[1/2] Synthesizing TTS audio (${segments.length} segments)...`);
|
|
417
|
+
|
|
418
|
+
const audioBuffers = [];
|
|
419
|
+
const skipped = []; // [{ index, text, message }]
|
|
420
|
+
const succeededSegments = [];
|
|
421
|
+
let lastQuota = null;
|
|
422
|
+
|
|
423
|
+
for (let i = 0; i < segments.length; i++) {
|
|
424
|
+
const seg = segments[i];
|
|
425
|
+
try {
|
|
426
|
+
const result = await synthesizeWithRetry({
|
|
427
|
+
apiBase: api, token,
|
|
428
|
+
text: seg.text,
|
|
429
|
+
voiceId: seg.voiceId || defaultVoice,
|
|
430
|
+
speed: seg.speed ?? defaultSpeed,
|
|
431
|
+
volume: seg.volume,
|
|
432
|
+
pitch: seg.pitch,
|
|
433
|
+
format,
|
|
434
|
+
index: i, total: segments.length,
|
|
435
|
+
});
|
|
436
|
+
audioBuffers.push(result.audio);
|
|
437
|
+
succeededSegments.push(seg);
|
|
438
|
+
lastQuota = result.quota;
|
|
439
|
+
} catch (err) {
|
|
440
|
+
// Hard-fail conditions: user must act, no point continuing.
|
|
441
|
+
// 'quota_exceeded' kept as alias to remain backward-compatible with
|
|
442
|
+
// older bundled CLI builds that still emit it (renamed to
|
|
443
|
+
// 'insufficient_quota' in 2026-05 to disambiguate from 'rate_limited').
|
|
444
|
+
if (err instanceof ApiError && [
|
|
445
|
+
'insufficient_quota', 'quota_exceeded',
|
|
446
|
+
'token_expired', 'network_error'
|
|
447
|
+
].includes(err.code)) {
|
|
448
|
+
throw err;
|
|
449
|
+
}
|
|
450
|
+
// Soft-fail: log + skip the segment, keep narrating the rest.
|
|
451
|
+
// Common cause: TTS provider rejects a particular text (e.g. Chinese voice
|
|
452
|
+
// on a pure-English short fragment left over from Markdown headings, see #2901).
|
|
453
|
+
const preview = seg.text.length > 60 ? `${seg.text.slice(0, 60)}…` : seg.text;
|
|
454
|
+
console.log(` ⚠ Segment ${i + 1} skipped: ${err.message}`);
|
|
455
|
+
console.log(` text: ${JSON.stringify(preview)}`);
|
|
456
|
+
skipped.push({ index: i + 1, text: seg.text, message: err.message });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (audioBuffers.length === 0) {
|
|
461
|
+
// All segments failed. Build a concrete, actionable error rather than just
|
|
462
|
+
// re-throwing the first message: list the offending segments and suggest
|
|
463
|
+
// the most common fix (#2901 — pad short Latin lines with Chinese context).
|
|
464
|
+
const lines = [
|
|
465
|
+
`All ${segments.length} segments failed TTS synthesis.`,
|
|
466
|
+
'',
|
|
467
|
+
'Failed segments:',
|
|
468
|
+
...skipped.map((s) => {
|
|
469
|
+
const preview = s.text.length > 60 ? `${s.text.slice(0, 60)}…` : s.text;
|
|
470
|
+
return ` [${s.index}] ${JSON.stringify(preview)}\n ${s.message}`;
|
|
471
|
+
}),
|
|
472
|
+
'',
|
|
473
|
+
'How to fix:',
|
|
474
|
+
' • Short pure-English lines (brand names, English headings) often fail',
|
|
475
|
+
' on Chinese TTS voices (Issue #2901). Pad with Chinese context, e.g.',
|
|
476
|
+
' "VoxFlow" → "VoxFlow 工具介绍" or merge into adjacent paragraphs.',
|
|
477
|
+
' • CLI already auto-retries with Chinese wrap; if that also failed, the',
|
|
478
|
+
' provider may be down — try `voxflow voices` to check connectivity.',
|
|
479
|
+
' • Re-run with -vv to see full TTS request/response for each segment.',
|
|
480
|
+
];
|
|
481
|
+
throw new Error(lines.join('\n'));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ─── Build WAV ─────────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
console.log('\n[2/2] Merging audio...');
|
|
487
|
+
const { audio, wav, duration } = format === 'mp3' || format === 'wav'
|
|
488
|
+
? concatAudioBuffers(audioBuffers, format, silence)
|
|
489
|
+
: buildWav(audioBuffers, silence);
|
|
490
|
+
|
|
491
|
+
const outBuffer = audio || wav;
|
|
492
|
+
|
|
493
|
+
// Write files
|
|
494
|
+
const outDir = path.dirname(outputPath);
|
|
495
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
496
|
+
fs.writeFileSync(outputPath, outBuffer);
|
|
497
|
+
|
|
498
|
+
const textPath = outputPath.replace(/\.(wav|mp3)$/i, '.txt');
|
|
499
|
+
const textContent = succeededSegments.map((s, i) => {
|
|
500
|
+
const prefix = s.voiceId ? `[${i + 1}|${s.voiceId}]` : `[${i + 1}]`;
|
|
501
|
+
return `${prefix} ${s.text}`;
|
|
502
|
+
}).join('\n\n');
|
|
503
|
+
fs.writeFileSync(textPath, textContent, 'utf8');
|
|
504
|
+
|
|
505
|
+
const quotaUsed = succeededSegments.length;
|
|
506
|
+
console.log(`\n=== Done ===`);
|
|
507
|
+
console.log(`Output: ${outputPath} (${(outBuffer.length / 1024).toFixed(1)} KB, ${duration.toFixed(1)}s)`);
|
|
508
|
+
console.log(`Transcript: ${textPath}`);
|
|
509
|
+
console.log(`Segments: ${succeededSegments.length}/${segments.length}${skipped.length ? ` (${skipped.length} skipped)` : ''}`);
|
|
510
|
+
console.log(`Quota: ${quotaUsed} used, ${lastQuota?.remaining ?? '?'} remaining`);
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
outputPath,
|
|
514
|
+
textPath,
|
|
515
|
+
duration,
|
|
516
|
+
quotaUsed,
|
|
517
|
+
segmentCount: succeededSegments.length,
|
|
518
|
+
skippedCount: skipped.length,
|
|
519
|
+
format,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ─── CLI Handler ────────────────────────────────────────────────────────────
|
|
524
|
+
|
|
525
|
+
async function handle(args) {
|
|
526
|
+
const { parseFlag, parseFloatFlag, validateSpeed, validateSilence, validateOutput, validateFormat, runWithRetry } = require('../core/args');
|
|
527
|
+
const { getToken, getTokenInfo } = require('../core/auth');
|
|
528
|
+
const { API_BASE } = require('../core/config');
|
|
529
|
+
|
|
530
|
+
const api = parseFlag(args, '--api') || API_BASE;
|
|
531
|
+
const explicitToken = parseFlag(args, '--token');
|
|
532
|
+
|
|
533
|
+
// Parse and validate before auth
|
|
534
|
+
const input = parseFlag(args, '--input');
|
|
535
|
+
const text = parseFlag(args, '--text');
|
|
536
|
+
const script = parseFlag(args, '--script');
|
|
537
|
+
const speed = parseFloatFlag(args, '--speed');
|
|
538
|
+
const silence = parseFloatFlag(args, '--silence');
|
|
539
|
+
const output = parseFlag(args, '--output', '-o');
|
|
540
|
+
const format = parseFlag(args, '--format');
|
|
541
|
+
|
|
542
|
+
validateSpeed(args, speed);
|
|
543
|
+
validateSilence(args, silence);
|
|
544
|
+
validateOutput(output, format);
|
|
545
|
+
validateFormat(format);
|
|
546
|
+
|
|
547
|
+
// Validate file existence for --input and --script
|
|
548
|
+
if (input) {
|
|
549
|
+
const fs = require('fs');
|
|
550
|
+
const path = require('path');
|
|
551
|
+
const resolved = path.resolve(input);
|
|
552
|
+
if (!fs.existsSync(resolved)) {
|
|
553
|
+
console.error(`Error: Input file not found: ${resolved}`);
|
|
554
|
+
// Common user mistake: passing inline text to --input. Detect it
|
|
555
|
+
// (no slash, no extension, contains non-ASCII or spaces) and suggest
|
|
556
|
+
// the right flag instead of just blaming "file not found".
|
|
557
|
+
const looksLikeText = !input.includes('/') &&
|
|
558
|
+
!/\.[a-z0-9]{1,4}$/i.test(input) &&
|
|
559
|
+
(/[^\x20-\x7e]/.test(input) || input.includes(' '));
|
|
560
|
+
if (looksLikeText) {
|
|
561
|
+
console.error('');
|
|
562
|
+
console.error(' Hint: --input expects a FILE PATH. For inline text use:');
|
|
563
|
+
console.error(` voxflow narrate --text ${JSON.stringify(input)}`);
|
|
564
|
+
console.error(` voxflow say ${JSON.stringify(input)} # simpler for one-liners`);
|
|
565
|
+
}
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (script) {
|
|
571
|
+
const fs = require('fs');
|
|
572
|
+
const path = require('path');
|
|
573
|
+
const resolved = path.resolve(script);
|
|
574
|
+
if (!fs.existsSync(resolved)) {
|
|
575
|
+
console.error(`Error: Script file not found: ${resolved}`);
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Authenticate after validation
|
|
581
|
+
let token;
|
|
582
|
+
if (explicitToken) {
|
|
583
|
+
token = explicitToken;
|
|
584
|
+
} else {
|
|
585
|
+
token = await getToken({ api });
|
|
586
|
+
const info = getTokenInfo();
|
|
587
|
+
if (info) {
|
|
588
|
+
console.log(`\x1b[32mLogged in as ${info.email}\x1b[0m`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const opts = {
|
|
593
|
+
token, api,
|
|
594
|
+
input, text, script,
|
|
595
|
+
voice: parseFlag(args, '--voice'),
|
|
596
|
+
output, speed, silence,
|
|
597
|
+
format: format || undefined,
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
await runWithRetry(narrate, opts, api, explicitToken);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const meta = {
|
|
604
|
+
narrate: {
|
|
605
|
+
usage: '[opts]',
|
|
606
|
+
description: 'Narrate a file, text, or script to audio',
|
|
607
|
+
options: [
|
|
608
|
+
`--input <file> Input .txt or .md file`,
|
|
609
|
+
`--text <text> Inline text to narrate`,
|
|
610
|
+
`--script <file> JSON script with per-segment voice/speed control`,
|
|
611
|
+
`--voice <id> Default voice ID (default: ${NARRATE_DEFAULTS.voice})`,
|
|
612
|
+
`--format <fmt> Output format: pcm, wav, mp3 (default: pcm → WAV)`,
|
|
613
|
+
`--speed <n> TTS speed 0.5-2.0 (default: ${NARRATE_DEFAULTS.speed})`,
|
|
614
|
+
`--silence <sec> Silence between segments, 0-5.0 (default: ${NARRATE_DEFAULTS.silence})`,
|
|
615
|
+
`--output <path> Output file path (default: matches input basename, e.g. design.md → design.wav)`,
|
|
616
|
+
],
|
|
617
|
+
examples: [
|
|
618
|
+
'voxflow narrate --input article.txt --voice v-female-R2s4N9qJ',
|
|
619
|
+
'voxflow narrate --script narration-script.json',
|
|
620
|
+
'echo "Hello" | voxflow narrate --output hello.wav',
|
|
621
|
+
],
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
module.exports = {
|
|
626
|
+
narrate,
|
|
627
|
+
handle,
|
|
628
|
+
meta,
|
|
629
|
+
ApiError,
|
|
630
|
+
_test: {
|
|
631
|
+
parseScript,
|
|
632
|
+
stripMarkdown,
|
|
633
|
+
scanSegmentRisks,
|
|
634
|
+
shouldAutoWrap,
|
|
635
|
+
chineseContextWrap,
|
|
636
|
+
synthesizeWithRetry,
|
|
637
|
+
isTtsProviderRejection,
|
|
638
|
+
},
|
|
639
|
+
};
|