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,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build a copy-pasteable AI prompt that asks the user's preferred AI tool
|
|
5
|
+
* (Claude Code / Cursor / ChatGPT / etc.) to edit one card of a stage deck.
|
|
6
|
+
*
|
|
7
|
+
* Two modes:
|
|
8
|
+
* - Card mode (default): rewrite the whole card.
|
|
9
|
+
* - Selection mode: rewrite a specific selected substring inside the card.
|
|
10
|
+
*
|
|
11
|
+
* The function is provider-agnostic on purpose. We do not generate
|
|
12
|
+
* Cursor-flavored diff prompts vs Claude-flavored natural-language prompts;
|
|
13
|
+
* one universal template is enough for v1. If usage data shows a real need
|
|
14
|
+
* for per-provider templates, ship a `~/.config/voxflow/edit-prompt-template.md`
|
|
15
|
+
* override later (see issue #3330).
|
|
16
|
+
*
|
|
17
|
+
* The output is intentionally readable to a human (so the user can sanity-
|
|
18
|
+
* check what they're about to send to their AI), and tells the AI it only
|
|
19
|
+
* needs to edit the file in place — the stage SSE will hot-reload.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} opts
|
|
22
|
+
* @param {string} opts.deckPath Absolute path to the deck JSON file.
|
|
23
|
+
* @param {number} opts.cardIdx Zero-based index of the card.
|
|
24
|
+
* @param {object} opts.card The card object (any shape — we just stringify it).
|
|
25
|
+
* @param {string} [opts.selectedText] Optional selected substring to focus on.
|
|
26
|
+
* @returns {string} The prompt, ready for clipboard.
|
|
27
|
+
*/
|
|
28
|
+
function buildEditPrompt(opts) {
|
|
29
|
+
const deckPath = String(opts && opts.deckPath || '').trim();
|
|
30
|
+
const cardIdx = Number.isFinite(opts && opts.cardIdx) ? opts.cardIdx : 0;
|
|
31
|
+
const card = (opts && opts.card && typeof opts.card === 'object') ? opts.card : {};
|
|
32
|
+
const selectedText = (opts && typeof opts.selectedText === 'string')
|
|
33
|
+
? opts.selectedText.trim() : '';
|
|
34
|
+
|
|
35
|
+
if (!deckPath) {
|
|
36
|
+
throw new Error('buildEditPrompt: deckPath is required');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const kind = (typeof card.kind === 'string' && card.kind) || 'unknown';
|
|
40
|
+
let cardJson;
|
|
41
|
+
try {
|
|
42
|
+
cardJson = JSON.stringify(card, null, 2);
|
|
43
|
+
} catch {
|
|
44
|
+
// Circular refs or non-serializable garbage — fall back to a minimal stub
|
|
45
|
+
// so the prompt still makes sense.
|
|
46
|
+
cardJson = JSON.stringify({ kind: card.kind || null, _note: 'card not stringifiable' }, null, 2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const header = [
|
|
50
|
+
"I'm iterating on a Slice deck (vertical 1080x1920 card video — narrated, theme-styled).",
|
|
51
|
+
'',
|
|
52
|
+
'File to edit:',
|
|
53
|
+
' ' + deckPath,
|
|
54
|
+
'',
|
|
55
|
+
'Card to change: cards[' + cardIdx + '] (kind: ' + kind + ')',
|
|
56
|
+
].join('\n');
|
|
57
|
+
|
|
58
|
+
const cardBlock = [
|
|
59
|
+
'Current card value:',
|
|
60
|
+
'```json',
|
|
61
|
+
cardJson,
|
|
62
|
+
'```',
|
|
63
|
+
].join('\n');
|
|
64
|
+
|
|
65
|
+
const footer = [
|
|
66
|
+
'When you finish, save the file in place — preserve every other field and the JSON structure.',
|
|
67
|
+
'The local stage server is watching this file and will hot-reload the browser preview within ~150ms.',
|
|
68
|
+
].join('\n');
|
|
69
|
+
|
|
70
|
+
if (selectedText) {
|
|
71
|
+
const selectionBlock = [
|
|
72
|
+
'Specifically, this exact substring needs to change:',
|
|
73
|
+
' ' + JSON.stringify(selectedText),
|
|
74
|
+
'',
|
|
75
|
+
'Replace only that substring; keep everything else in the card identical unless the change requires touching adjacent text.',
|
|
76
|
+
].join('\n');
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
header,
|
|
80
|
+
'',
|
|
81
|
+
selectionBlock,
|
|
82
|
+
'',
|
|
83
|
+
cardBlock,
|
|
84
|
+
'',
|
|
85
|
+
'Change request:',
|
|
86
|
+
' <REPLACE THIS WITH YOUR INSTRUCTION — e.g. "make it punchier", "shorten to 12 chars">',
|
|
87
|
+
'',
|
|
88
|
+
footer,
|
|
89
|
+
].join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [
|
|
93
|
+
header,
|
|
94
|
+
'',
|
|
95
|
+
cardBlock,
|
|
96
|
+
'',
|
|
97
|
+
'Change request:',
|
|
98
|
+
' <REPLACE THIS WITH YOUR INSTRUCTION — e.g. "rewrite caption + narration in a more visceral tone">',
|
|
99
|
+
'',
|
|
100
|
+
footer,
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { buildEditPrompt };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tiny pub/sub bus shared between the file watcher and SSE response streams.
|
|
5
|
+
*
|
|
6
|
+
* Subscribers register a callback; publish(event) fans out synchronously.
|
|
7
|
+
* Used to decouple the watcher (writes) from the HTTP server (reads).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function createEventBus() {
|
|
11
|
+
const listeners = new Set();
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
subscribe(fn) {
|
|
15
|
+
listeners.add(fn);
|
|
16
|
+
return function unsubscribe() {
|
|
17
|
+
listeners.delete(fn);
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
publish(event) {
|
|
21
|
+
for (const fn of listeners) {
|
|
22
|
+
try { fn(event); } catch { /* one bad subscriber must not break others */ }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
size() {
|
|
26
|
+
return listeners.size;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { createEventBus };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const net = require('net');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Probe a single port on 127.0.0.1. Resolves true if listen() succeeds.
|
|
7
|
+
*/
|
|
8
|
+
function probePort(port) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const srv = net.createServer();
|
|
11
|
+
srv.unref();
|
|
12
|
+
srv.once('error', () => resolve(false));
|
|
13
|
+
srv.once('listening', () => {
|
|
14
|
+
srv.close(() => resolve(true));
|
|
15
|
+
});
|
|
16
|
+
try {
|
|
17
|
+
srv.listen({ port, host: '127.0.0.1', exclusive: true });
|
|
18
|
+
} catch {
|
|
19
|
+
resolve(false);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find a free port starting at `preferred`, scanning up to `maxTries` ports.
|
|
26
|
+
*
|
|
27
|
+
* Stage default is 5180 — already inside the studio CORS allowlist (5173-5186)
|
|
28
|
+
* but distinct from Vite (5173) so a parallel `studio dev` does not clash.
|
|
29
|
+
*
|
|
30
|
+
* @param {number} preferred Starting port
|
|
31
|
+
* @param {number} maxTries Max sequential ports to try (default 10 → 5180-5189)
|
|
32
|
+
* @returns {Promise<number>} Available port
|
|
33
|
+
* @throws if every candidate is occupied
|
|
34
|
+
*/
|
|
35
|
+
async function findAvailablePort(preferred = 5180, maxTries = 10) {
|
|
36
|
+
for (let i = 0; i < maxTries; i++) {
|
|
37
|
+
const candidate = preferred + i;
|
|
38
|
+
if (await probePort(candidate)) return candidate;
|
|
39
|
+
}
|
|
40
|
+
throw new Error(
|
|
41
|
+
`No free port in [${preferred}, ${preferred + maxTries - 1}]. ` +
|
|
42
|
+
`Pass --port <n> or free a port and retry.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { findAvailablePort, probePort };
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const { findAvailablePort } = require('./port');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Start the Stage HTTP server.
|
|
8
|
+
*
|
|
9
|
+
* Routes (MVP):
|
|
10
|
+
* GET / → serves the per-module Stage UI (passed in)
|
|
11
|
+
* GET /healthz → liveness probe (200 "ok")
|
|
12
|
+
* GET /api/deck → returns the current deck JSON (or 404 + reason)
|
|
13
|
+
* GET /events → Server-Sent Events stream (push deck updates)
|
|
14
|
+
*
|
|
15
|
+
* SSE was chosen over WebSocket to avoid a `ws` runtime dep (the CLI keeps
|
|
16
|
+
* its dep list at exactly two — `open`, `pptxgenjs`). Stage only needs
|
|
17
|
+
* server→client push; client→server actions go through plain HTTP POST when
|
|
18
|
+
* we add Phase 1.3+ (renderStill, publish).
|
|
19
|
+
*
|
|
20
|
+
* @param {object} opts
|
|
21
|
+
* @param {string} opts.uiHtml Static HTML served at /
|
|
22
|
+
* @param {() => {deck:object|null, error?:string, sourcePath:string}} opts.getDeckSnapshot
|
|
23
|
+
* @param {(handler: (event:object) => void) => () => void} opts.subscribe
|
|
24
|
+
* Subscribe to deck-change events (returns unsubscribe fn)
|
|
25
|
+
* @param {object} [opts.snapshots] Optional snapshot store handle.
|
|
26
|
+
* If provided, exposes /api/snapshots, /api/snapshots/:id, and
|
|
27
|
+
* POST /api/snapshots/restore. Pass `null` (or omit) to disable history.
|
|
28
|
+
* Shape: { list(), load(id), restore(id) }. `restore` writes the snapshot
|
|
29
|
+
* bytes back to the deck file and returns the restored deck object.
|
|
30
|
+
* @param {object} [opts.cloudRender] Optional cloud-render client handle.
|
|
31
|
+
* If provided, exposes POST /api/render-mp4, GET /api/render-status/:id,
|
|
32
|
+
* and GET /api/quota-balance — all proxies to the upstream API. Shape:
|
|
33
|
+
* { submit(deck), status(jobId), quota() }. Lets users go from "deck I
|
|
34
|
+
* like" → mp4 without leaving stage; the proxy keeps the JWT off the page.
|
|
35
|
+
* @param {number} [opts.preferredPort=5180]
|
|
36
|
+
* @param {number} [opts.maxPortTries=10]
|
|
37
|
+
* @returns {Promise<{server:http.Server, port:number, url:string, close:() => Promise<void>}>}
|
|
38
|
+
*/
|
|
39
|
+
async function startStageServer(opts) {
|
|
40
|
+
const {
|
|
41
|
+
uiHtml,
|
|
42
|
+
getDeckSnapshot,
|
|
43
|
+
subscribe,
|
|
44
|
+
snapshots = null,
|
|
45
|
+
cloudRender = null,
|
|
46
|
+
preferredPort = 5180,
|
|
47
|
+
maxPortTries = 10,
|
|
48
|
+
} = opts;
|
|
49
|
+
|
|
50
|
+
if (typeof uiHtml !== 'string' || !uiHtml) {
|
|
51
|
+
throw new Error('startStageServer: uiHtml (string) is required');
|
|
52
|
+
}
|
|
53
|
+
if (typeof getDeckSnapshot !== 'function') {
|
|
54
|
+
throw new Error('startStageServer: getDeckSnapshot (fn) is required');
|
|
55
|
+
}
|
|
56
|
+
if (typeof subscribe !== 'function') {
|
|
57
|
+
throw new Error('startStageServer: subscribe (fn) is required');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const port = await findAvailablePort(preferredPort, maxPortTries);
|
|
61
|
+
|
|
62
|
+
const sseClients = new Set();
|
|
63
|
+
|
|
64
|
+
// Defense against drive-by requests from other browser tabs. Any tab on
|
|
65
|
+
// http://evil.com can `fetch('http://127.0.0.1:<port>/api/deck')` —
|
|
66
|
+
// binding to 127.0.0.1 stops external networks but not the local browser.
|
|
67
|
+
// Cross-origin fetch/XHR/EventSource always sends Origin; top-level
|
|
68
|
+
// navigation and curl do not. So: missing Origin = allow (address bar /
|
|
69
|
+
// curl), present + matches loopback host:port = allow, anything else = 403.
|
|
70
|
+
function isAllowedOrigin(originHeader) {
|
|
71
|
+
if (!originHeader) return true;
|
|
72
|
+
let url;
|
|
73
|
+
try { url = new URL(originHeader); } catch { return false; }
|
|
74
|
+
if (url.protocol !== 'http:') return false;
|
|
75
|
+
const hostOk = url.hostname === '127.0.0.1'
|
|
76
|
+
|| url.hostname === 'localhost'
|
|
77
|
+
|| url.hostname === '::1';
|
|
78
|
+
return hostOk && url.port === String(port);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const server = http.createServer((req, res) => {
|
|
82
|
+
if (!isAllowedOrigin(req.headers.origin)) {
|
|
83
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
84
|
+
res.end(JSON.stringify({
|
|
85
|
+
code: 'forbidden_origin',
|
|
86
|
+
message: `Origin ${req.headers.origin} is not allowed; stage server only accepts loopback origins on port ${port}.`,
|
|
87
|
+
}));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (req.method === 'GET' && req.url === '/') {
|
|
92
|
+
res.writeHead(200, {
|
|
93
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
94
|
+
'Cache-Control': 'no-store',
|
|
95
|
+
});
|
|
96
|
+
res.end(uiHtml);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (req.method === 'GET' && req.url === '/healthz') {
|
|
101
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
102
|
+
res.end('ok');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (req.method === 'GET' && req.url === '/api/deck') {
|
|
107
|
+
const snap = getDeckSnapshot();
|
|
108
|
+
if (!snap.deck) {
|
|
109
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
110
|
+
res.end(JSON.stringify({
|
|
111
|
+
code: 'deck_unavailable',
|
|
112
|
+
message: snap.error || 'No deck loaded yet.',
|
|
113
|
+
sourcePath: snap.sourcePath,
|
|
114
|
+
}));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
res.writeHead(200, {
|
|
118
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
119
|
+
'Cache-Control': 'no-store',
|
|
120
|
+
});
|
|
121
|
+
res.end(JSON.stringify({
|
|
122
|
+
code: 'success',
|
|
123
|
+
sourcePath: snap.sourcePath,
|
|
124
|
+
deck: snap.deck,
|
|
125
|
+
}));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Snapshot history (optional; only mounted when `snapshots` passed) ──
|
|
130
|
+
if (snapshots && req.method === 'GET' && req.url === '/api/snapshots') {
|
|
131
|
+
const versions = snapshots.list();
|
|
132
|
+
res.writeHead(200, {
|
|
133
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
134
|
+
'Cache-Control': 'no-store',
|
|
135
|
+
});
|
|
136
|
+
res.end(JSON.stringify({
|
|
137
|
+
code: 'success',
|
|
138
|
+
storePath: snapshots.getStorePath ? snapshots.getStorePath() : '',
|
|
139
|
+
versions,
|
|
140
|
+
}));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (snapshots && req.method === 'GET' && req.url.startsWith('/api/snapshots/')) {
|
|
145
|
+
const id = req.url.slice('/api/snapshots/'.length);
|
|
146
|
+
const deck = snapshots.load(id);
|
|
147
|
+
if (!deck) {
|
|
148
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
149
|
+
res.end(JSON.stringify({ code: 'snapshot_not_found', message: `No snapshot: ${id}` }));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
res.writeHead(200, {
|
|
153
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
154
|
+
'Cache-Control': 'no-store',
|
|
155
|
+
});
|
|
156
|
+
res.end(JSON.stringify({ code: 'success', id, deck }));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (snapshots && req.method === 'POST' && req.url === '/api/snapshots/restore') {
|
|
161
|
+
const chunks = [];
|
|
162
|
+
let totalBytes = 0;
|
|
163
|
+
req.on('data', (chunk) => {
|
|
164
|
+
totalBytes += chunk.length;
|
|
165
|
+
// Hard cap; restore body is just `{ "id": "..." }` (~80 bytes)
|
|
166
|
+
if (totalBytes > 1024) {
|
|
167
|
+
req.destroy();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
chunks.push(chunk);
|
|
171
|
+
});
|
|
172
|
+
req.on('end', () => {
|
|
173
|
+
let body;
|
|
174
|
+
try { body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'); }
|
|
175
|
+
catch {
|
|
176
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
177
|
+
res.end(JSON.stringify({ code: 'bad_json', message: 'restore body must be JSON' }));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const id = body && typeof body.id === 'string' ? body.id : null;
|
|
181
|
+
if (!id || !snapshots.restore) {
|
|
182
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
183
|
+
res.end(JSON.stringify({ code: 'invalid_id', message: 'missing or invalid id' }));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
let result;
|
|
187
|
+
try { result = snapshots.restore(id); }
|
|
188
|
+
catch (err) {
|
|
189
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
190
|
+
res.end(JSON.stringify({ code: 'restore_failed', message: err.message || 'restore error' }));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (!result || !result.deck) {
|
|
194
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
195
|
+
res.end(JSON.stringify({ code: 'snapshot_not_found', message: `No snapshot: ${id}` }));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
199
|
+
res.end(JSON.stringify({ code: 'success', id, deck: result.deck }));
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── Cloud render proxy (optional) ───────────────────────────────────────
|
|
205
|
+
// POST /api/render-mp4 body { deck } → submit a cloud render job
|
|
206
|
+
// GET /api/render-status/:id → poll job status / video url
|
|
207
|
+
// GET /api/quota-balance → read remaining quota for confirm
|
|
208
|
+
//
|
|
209
|
+
// The proxy talks to the backend with the local CLI token; the page
|
|
210
|
+
// never sees the JWT. All three are off-by-default — only mounted when
|
|
211
|
+
// a cloudRender client was passed at construction.
|
|
212
|
+
function sendJson(status, payload) {
|
|
213
|
+
res.writeHead(status, {
|
|
214
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
215
|
+
'Cache-Control': 'no-store',
|
|
216
|
+
});
|
|
217
|
+
res.end(JSON.stringify(payload));
|
|
218
|
+
}
|
|
219
|
+
function statusForCode(code) {
|
|
220
|
+
if (code === 'not_logged_in') return 401;
|
|
221
|
+
if (code === 'quota_exceeded') return 402;
|
|
222
|
+
if (code === 'invalid_deck' || code === 'invalid_id') return 400;
|
|
223
|
+
if (code === 'job_not_found') return 404;
|
|
224
|
+
if (code === 'success') return 200;
|
|
225
|
+
return 502; // upstream error
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (cloudRender && req.method === 'POST' && req.url === '/api/render-mp4') {
|
|
229
|
+
const chunks = [];
|
|
230
|
+
let total = 0;
|
|
231
|
+
// Decks are small (5-8 cards, ~5 KB). 64 KB cap defends against pasted
|
|
232
|
+
// monsters without affecting the realistic upper bound.
|
|
233
|
+
const MAX = 64 * 1024;
|
|
234
|
+
req.on('data', (c) => {
|
|
235
|
+
total += c.length;
|
|
236
|
+
if (total > MAX) { req.destroy(); return; }
|
|
237
|
+
chunks.push(c);
|
|
238
|
+
});
|
|
239
|
+
req.on('end', async () => {
|
|
240
|
+
let body;
|
|
241
|
+
try { body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'); }
|
|
242
|
+
catch { return sendJson(400, { code: 'bad_json', message: 'render body must be JSON' }); }
|
|
243
|
+
const deck = body && body.deck;
|
|
244
|
+
if (!deck || !Array.isArray(deck.cards)) {
|
|
245
|
+
return sendJson(400, { code: 'invalid_deck', message: 'deck.cards required' });
|
|
246
|
+
}
|
|
247
|
+
let result;
|
|
248
|
+
try { result = await cloudRender.submit(deck); }
|
|
249
|
+
catch (err) { return sendJson(502, { code: 'upstream_error', message: err.message }); }
|
|
250
|
+
return sendJson(statusForCode(result.code), result);
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (cloudRender && req.method === 'GET' && req.url.startsWith('/api/render-status/')) {
|
|
256
|
+
const id = decodeURIComponent(req.url.slice('/api/render-status/'.length));
|
|
257
|
+
(async () => {
|
|
258
|
+
let result;
|
|
259
|
+
try { result = await cloudRender.status(id); }
|
|
260
|
+
catch (err) { return sendJson(502, { code: 'upstream_error', message: err.message }); }
|
|
261
|
+
return sendJson(statusForCode(result.code), result);
|
|
262
|
+
})();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (cloudRender && req.method === 'GET' && req.url === '/api/quota-balance') {
|
|
267
|
+
(async () => {
|
|
268
|
+
let result;
|
|
269
|
+
try { result = await cloudRender.quota(); }
|
|
270
|
+
catch (err) { return sendJson(502, { code: 'upstream_error', message: err.message }); }
|
|
271
|
+
return sendJson(statusForCode(result.code), result);
|
|
272
|
+
})();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (req.method === 'GET' && req.url === '/events') {
|
|
277
|
+
res.writeHead(200, {
|
|
278
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
279
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
280
|
+
'Connection': 'keep-alive',
|
|
281
|
+
'X-Accel-Buffering': 'no',
|
|
282
|
+
});
|
|
283
|
+
// Initial frame so EventSource resolves "open" promptly.
|
|
284
|
+
res.write(`: stage stream open\n\n`);
|
|
285
|
+
|
|
286
|
+
// Push the current snapshot immediately so the UI does not have to
|
|
287
|
+
// race the first watcher event.
|
|
288
|
+
const snap = getDeckSnapshot();
|
|
289
|
+
const initial = {
|
|
290
|
+
type: snap.deck ? 'deck' : 'error',
|
|
291
|
+
sourcePath: snap.sourcePath,
|
|
292
|
+
deck: snap.deck || null,
|
|
293
|
+
error: snap.error || null,
|
|
294
|
+
ts: Date.now(),
|
|
295
|
+
};
|
|
296
|
+
res.write(`event: ${initial.type}\n`);
|
|
297
|
+
res.write(`data: ${JSON.stringify(initial)}\n\n`);
|
|
298
|
+
|
|
299
|
+
const unsubscribe = subscribe((event) => {
|
|
300
|
+
try {
|
|
301
|
+
res.write(`event: ${event.type}\n`);
|
|
302
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
303
|
+
} catch {
|
|
304
|
+
// Connection closed under us; teardown handler will clean up.
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const heartbeat = setInterval(() => {
|
|
309
|
+
try { res.write(`: heartbeat\n\n`); } catch { /* dead */ }
|
|
310
|
+
}, 15_000);
|
|
311
|
+
heartbeat.unref?.();
|
|
312
|
+
|
|
313
|
+
const teardown = () => {
|
|
314
|
+
clearInterval(heartbeat);
|
|
315
|
+
unsubscribe();
|
|
316
|
+
sseClients.delete(res);
|
|
317
|
+
try { res.end(); } catch { /* already ended */ }
|
|
318
|
+
};
|
|
319
|
+
req.on('close', teardown);
|
|
320
|
+
req.on('aborted', teardown);
|
|
321
|
+
sseClients.add(res);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
326
|
+
res.end(JSON.stringify({ code: 'not_found', message: `No route: ${req.method} ${req.url}` }));
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await new Promise((resolve, reject) => {
|
|
330
|
+
server.once('error', reject);
|
|
331
|
+
server.listen(port, '127.0.0.1', () => {
|
|
332
|
+
server.removeListener('error', reject);
|
|
333
|
+
resolve();
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
server,
|
|
339
|
+
port,
|
|
340
|
+
url: `http://127.0.0.1:${port}`,
|
|
341
|
+
async close() {
|
|
342
|
+
// End all live SSE streams so server.close() does not hang.
|
|
343
|
+
for (const res of sseClients) {
|
|
344
|
+
try { res.end(); } catch { /* already gone */ }
|
|
345
|
+
}
|
|
346
|
+
sseClients.clear();
|
|
347
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = { startStageServer };
|