refactor(tts): replace Bun-specific APIs with execa and Node.js stdlib

- Replace `$` and `Bun.spawn` with `execa` for process execution
- Swap `Bun.JSONL.parse` for native `JSON.parse` and use `node:readline` for stdin handling
This commit is contained in:
mitchell 2026-06-07 22:13:19 -04:00
parent 4912864a75
commit 3235a08633
3 changed files with 277 additions and 83 deletions

View file

@ -28,7 +28,8 @@
"ai": "^6.0.161",
"ansi-styles": "^6.2.3",
"chalk": "^5.6.2",
"happy-dom-without-node": "^14.12.3",
"execa": "^9.6.1",
"happy-dom": "^20.10.2",
"marked": "^18.0.4",
"rolldown": "^1.0.3",
"shiki": "^4.0.2",

268
pnpm-lock.yaml generated
View file

@ -19,7 +19,7 @@ importers:
version: 1.4.0
'@cliffy/table':
specifier: npm:@jsr/cliffy__table
version: '@jsr/cliffy__table@1.1.1'
version: '@jsr/cliffy__table@1.2.1'
'@mozilla/readability':
specifier: ^0.6.0
version: 0.6.0
@ -35,9 +35,12 @@ importers:
chalk:
specifier: ^5.6.2
version: 5.6.2
happy-dom-without-node:
specifier: ^14.12.3
version: 14.12.3
execa:
specifier: ^9.6.1
version: 9.6.1
happy-dom:
specifier: ^20.10.2
version: 20.10.2
marked:
specifier: ^18.0.4
version: 18.0.4
@ -158,8 +161,8 @@ packages:
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@jsr/cliffy__table@1.1.1':
resolution: {integrity: sha512-LcmLDrR/5uUzNklMNCqIK2L8RYdyyX27/QydFxxzi+HeB5sMyjOWOI636HPbYBGcr+gB7xlNGRi3EYgBXZnfWg==, tarball: https://npm.jsr.io/~/11/@jsr/cliffy__table/1.1.1.tgz}
'@jsr/cliffy__table@1.2.1':
resolution: {integrity: sha512-C/edvcMwtnbFx0zyoeGRcC+ZRvGYqxEhRi19HpfbfiYS4ZErKjEwMJmavO/DN7Wxbk34UpJP+UQKL7nKe+dYkA==, tarball: https://npm.jsr.io/~/11/@jsr/cliffy__table/1.2.1.tgz}
'@jsr/std__fmt@1.0.10':
resolution: {integrity: sha512-Q2N8XDNkh/c1IKTi+sO4Vm64M5m4W0o47qTURmMX0vgHm9AJ6jL7jEQ2kw0M1T2rBZiwAY78eUbYzFyOYWaCqQ==, tarball: https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.10.tgz}
@ -279,6 +282,9 @@ packages:
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@shikijs/core@4.1.0':
resolution: {integrity: sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ==}
engines: {node: '>=20'}
@ -310,6 +316,10 @@ packages:
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@sindresorhus/merge-streams@4.0.0':
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@ -331,6 +341,12 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/whatwg-mimetype@3.0.2':
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@ungap/structured-clone@1.3.1':
resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==}
@ -356,6 +372,10 @@ packages:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
buffer-image-size@0.6.4:
resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
engines: {node: '>=4.0'}
bun-types@1.3.14:
resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==}
@ -375,6 +395,10 @@ packages:
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@ -382,8 +406,8 @@ packages:
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
environment@1.1.0:
@ -394,6 +418,10 @@ packages:
resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==}
engines: {node: '>=18.0.0'}
execa@9.6.1:
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
engines: {node: ^18.19.0 || >=20.5.0}
fast-string-truncated-width@3.0.3:
resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
@ -403,13 +431,21 @@ packages:
fast-wrap-ansi@0.2.2:
resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
figures@6.1.0:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'}
get-east-asian-width@1.6.0:
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
engines: {node: '>=18'}
happy-dom-without-node@14.12.3:
resolution: {integrity: sha512-1wp8+GFneT8mBjVnzancXHRuscEUH3vnb38lfCHPxuSu6OiRw1kQzHFbTGYlNCU2YXOlVIlzsS6xg9nAr7Xg6Q==}
engines: {node: '>=16.0.0'}
get-stream@9.0.1:
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
engines: {node: '>=18'}
happy-dom@20.10.2:
resolution: {integrity: sha512-5p9Sxis3eowDJKqx90QCsgbNA02XXqJ59NOHvD4V6cxp+rP4d/xOyVx7uY3hS8hiUbY1VeiFH8lbJ81AyuDVLQ==}
engines: {node: '>=20.0.0'}
has-flag@5.0.1:
resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==}
@ -424,6 +460,25 @@ packages:
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
human-signals@8.0.1:
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
engines: {node: '>=18.18.0'}
is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
is-stream@4.0.1:
resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
engines: {node: '>=18'}
is-unicode-supported@2.1.0:
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
engines: {node: '>=18'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
json-schema@0.4.0:
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
@ -450,19 +505,35 @@ packages:
micromark-util-types@2.0.2:
resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
npm-run-path@6.0.0:
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
engines: {node: '>=18'}
oniguruma-parser@0.12.2:
resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
oniguruma-to-es@4.3.6:
resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==}
parse-ms@4.0.0:
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
engines: {node: '>=18'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-key@4.0.0:
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
engines: {node: '>=12'}
pretty-ms@9.3.0:
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
engines: {node: '>=18'}
property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
regex-recursion@6.0.2:
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
@ -477,10 +548,22 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
shiki@4.1.0:
resolution: {integrity: sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q==}
engines: {node: '>=20'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
@ -498,6 +581,10 @@ packages:
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
engines: {node: '>=12'}
strip-final-newline@4.0.0:
resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
engines: {node: '>=18'}
supports-color@10.2.2:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
@ -510,10 +597,6 @@ packages:
resolution: {integrity: sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==}
engines: {node: '>=20'}
tr46@5.1.1:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@ -528,6 +611,10 @@ packages:
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
unicorn-magic@0.3.0:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
engines: {node: '>=18'}
unist-util-is@6.0.1:
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
@ -549,22 +636,35 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'}
whatwg-url@14.2.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
wrap-ansi@10.0.0:
resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==}
engines: {node: '>=20'}
ws@8.21.0:
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
yoctocolors@2.1.2:
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
engines: {node: '>=18'}
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
@ -660,7 +760,7 @@ snapshots:
tslib: 2.8.1
optional: true
'@jsr/cliffy__table@1.1.1':
'@jsr/cliffy__table@1.2.1':
dependencies:
'@jsr/std__fmt': 1.0.10
@ -730,6 +830,8 @@ snapshots:
'@rolldown/pluginutils@1.0.1': {}
'@sec-ant/readable-stream@0.4.1': {}
'@shikijs/core@4.1.0':
dependencies:
'@shikijs/primitive': 4.1.0
@ -770,6 +872,8 @@ snapshots:
'@shikijs/vscode-textmate@10.0.2': {}
'@sindresorhus/merge-streams@4.0.0': {}
'@standard-schema/spec@1.1.0': {}
'@tybys/wasm-util@0.10.2':
@ -795,6 +899,12 @@ snapshots:
'@types/unist@3.0.3': {}
'@types/whatwg-mimetype@3.0.2': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 25.9.1
'@ungap/structured-clone@1.3.1': {}
'@vercel/oidc@3.2.0': {}
@ -815,6 +925,10 @@ snapshots:
ansi-styles@6.2.3: {}
buffer-image-size@0.6.4:
dependencies:
'@types/node': 25.9.1
bun-types@1.3.14:
dependencies:
'@types/node': 25.9.1
@ -829,18 +943,39 @@ snapshots:
comma-separated-tokens@2.0.3: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
dequal@2.0.3: {}
devlop@1.1.0:
dependencies:
dequal: 2.0.3
entities@4.5.0: {}
entities@7.0.1: {}
environment@1.1.0: {}
eventsource-parser@3.0.8: {}
execa@9.6.1:
dependencies:
'@sindresorhus/merge-streams': 4.0.0
cross-spawn: 7.0.6
figures: 6.1.0
get-stream: 9.0.1
human-signals: 8.0.1
is-plain-obj: 4.1.0
is-stream: 4.0.1
npm-run-path: 6.0.0
pretty-ms: 9.3.0
signal-exit: 4.1.0
strip-final-newline: 4.0.0
yoctocolors: 2.1.2
fast-string-truncated-width@3.0.3: {}
fast-string-width@3.0.2:
@ -851,14 +986,29 @@ snapshots:
dependencies:
fast-string-width: 3.0.2
figures@6.1.0:
dependencies:
is-unicode-supported: 2.1.0
get-east-asian-width@1.6.0: {}
happy-dom-without-node@14.12.3:
get-stream@9.0.1:
dependencies:
entities: 4.5.0
webidl-conversions: 7.0.0
'@sec-ant/readable-stream': 0.4.1
is-stream: 4.0.1
happy-dom@20.10.2:
dependencies:
'@types/node': 25.9.1
'@types/whatwg-mimetype': 3.0.2
'@types/ws': 8.18.1
buffer-image-size: 0.6.4
entities: 7.0.1
whatwg-mimetype: 3.0.0
whatwg-url: 14.2.0
ws: 8.21.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
has-flag@5.0.1: {}
@ -882,6 +1032,16 @@ snapshots:
html-void-elements@3.0.0: {}
human-signals@8.0.1: {}
is-plain-obj@4.1.0: {}
is-stream@4.0.1: {}
is-unicode-supported@2.1.0: {}
isexe@2.0.0: {}
json-schema@0.4.0: {}
marked@18.0.4: {}
@ -915,6 +1075,11 @@ snapshots:
micromark-util-types@2.0.2: {}
npm-run-path@6.0.0:
dependencies:
path-key: 4.0.0
unicorn-magic: 0.3.0
oniguruma-parser@0.12.2: {}
oniguruma-to-es@4.3.6:
@ -923,9 +1088,17 @@ snapshots:
regex: 6.1.0
regex-recursion: 6.0.2
property-information@7.1.0: {}
parse-ms@4.0.0: {}
punycode@2.3.1: {}
path-key@3.1.1: {}
path-key@4.0.0: {}
pretty-ms@9.3.0:
dependencies:
parse-ms: 4.0.0
property-information@7.1.0: {}
regex-recursion@6.0.2:
dependencies:
@ -958,6 +1131,12 @@ snapshots:
'@rolldown/binding-win32-arm64-msvc': 1.0.3
'@rolldown/binding-win32-x64-msvc': 1.0.3
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
shiki@4.1.0:
dependencies:
'@shikijs/core': 4.1.0
@ -969,6 +1148,8 @@ snapshots:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
signal-exit@4.1.0: {}
sisteransi@1.0.5: {}
space-separated-tokens@2.0.2: {}
@ -987,6 +1168,8 @@ snapshots:
dependencies:
ansi-regex: 6.2.2
strip-final-newline@4.0.0: {}
supports-color@10.2.2: {}
supports-hyperlinks@4.4.0:
@ -999,10 +1182,6 @@ snapshots:
ansi-escapes: 7.3.0
supports-hyperlinks: 4.4.0
tr46@5.1.1:
dependencies:
punycode: 2.3.1
trim-lines@3.0.1: {}
tslib@2.8.1:
@ -1012,6 +1191,8 @@ snapshots:
undici-types@7.24.6: {}
unicorn-magic@0.3.0: {}
unist-util-is@6.0.1:
dependencies:
'@types/unist': 3.0.3
@ -1045,14 +1226,11 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
webidl-conversions@7.0.0: {}
whatwg-mimetype@3.0.0: {}
whatwg-url@14.2.0:
which@2.0.2:
dependencies:
tr46: 5.1.1
webidl-conversions: 7.0.0
isexe: 2.0.0
wrap-ansi@10.0.0:
dependencies:
@ -1060,6 +1238,10 @@ snapshots:
string-width: 8.2.1
strip-ansi: 7.2.0
ws@8.21.0: {}
yoctocolors@2.1.2: {}
zod@4.4.3: {}
zwitch@2.0.4: {}

71
tts.ts
View file

@ -1,15 +1,17 @@
import os from "node:os";
import process from "node:process";
import { createInterface } from "node:readline";
import { parseArgs } from "node:util";
import { Readability } from "@mozilla/readability";
import { $, which } from "bun";
import chalk from "chalk";
import { Window } from "happy-dom-without-node";
import { execa } from "execa";
import { Window } from "happy-dom";
import wrapAnsi from "wrap-ansi";
const start = Date.now();
const { values: args } = parseArgs({
args: Bun.argv,
args: process.argv,
options: {
wait: { type: "boolean" },
w: { type: "boolean" },
@ -22,7 +24,15 @@ const { values: args } = parseArgs({
const wait = args.wait ?? args.w;
const url = args.url ?? args.u;
let input = url ? url : await Bun.stdin.text();
let input = url
? url
: await new Promise<string>((resolve) => {
let data = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => (data += chunk));
process.stdin.on("end", () => resolve(data));
});
if (url !== undefined) {
const result = await fetch(input.trim());
@ -45,9 +55,8 @@ if (!input) {
const home = os.homedir();
const cwd = `${home}/.ttsc/`;
$.cwd(cwd);
await $`rm *.wav`.nothrow();
await execa({ cwd, shell: true, reject: false })`rm *.wav`;
console.log(chalk.blue("Welcome to TTSC!"));
console.log(
@ -56,16 +65,16 @@ console.log(
),
);
const koko = Bun.spawn({
cmd: ["uv", "run", "--", "python", "-u", "main.py"],
const koko = execa({
cwd,
stdin: Buffer.from(input),
input: Buffer.from(input), // execa v6+ uses 'input'. Use 'stdin' for v5
stdout: "pipe",
stderr: "inherit",
});
})`uv run -- python -u main.py`;
if (!wait) {
koko.exited.then(() => {
koko.catch(() => {});
koko.then(() => {
const stop = Date.now();
const duration = ((stop - start) / 1000).toFixed(2);
console.log(chalk.green(`\nProcessing finished: ${duration}s`));
@ -73,15 +82,17 @@ if (!wait) {
}
let first = true;
const decoder = new TextDecoder();
for await (const line of koko.stdout) {
const decoded = decoder.decode(line).trim();
const rl = createInterface({ input: koko.stdout, crlfDelay: Infinity });
let infos: { file: number; text: string }[];
for await (const line of rl) {
const trimmed = line.trim();
if (!trimmed) continue;
let info: { file: number; text: string };
try {
infos = Bun.JSONL.parse(decoded) as { file: number; text: string }[];
info = JSON.parse(trimmed);
} catch {
console.log(decoded);
console.log(trimmed);
continue;
}
@ -92,25 +103,22 @@ for await (const line of koko.stdout) {
console.log(chalk.green(`\nTime-to-audio: ${duration}s`));
}
for (const info of infos) {
const [columns] = process.stdout.getWindowSize();
const text = Bun.wrapAnsi(info.text, columns);
const text = wrapAnsi(info.text, columns);
const file = `${home}/.ttsc/${info.file}.wav`;
console.log(`\n${chalk.blue(file)}:\n${text}\n`);
if (!wait) {
await Bun.spawn({
cmd: ["mpv", file],
await execa({
cwd,
stdin: "ignore",
stdout: "inherit",
stderr: "inherit",
}).exited;
}
})`mpv ${file}`;
}
}
await koko.exited;
await koko;
if (!wait) process.exit();
const stop = Date.now();
@ -118,19 +126,22 @@ const duration = ((stop - start) / 1000).toFixed(2);
console.log(chalk.green(`\nProcessing finished: ${duration}s`));
console.log(chalk.yellow(`Combining audio with sox...`));
if (which("sox")) {
await $`sox *.wav output.wav`;
try {
await execa({ cwd, shell: true })`sox *.wav output.wav`;
console.log(chalk.green(`Playing audio from ${cwd}output.wav`));
} else {
} catch (err) {
if (err instanceof Object && "code" in err && err.code === "ENOENT") {
console.error("Sox not found. Unable to combine outputs.");
} else {
throw err;
}
console.log(chalk.yellow(`Audio available in ${cwd}`));
process.exit();
}
await Bun.spawn({
cmd: ["mpv", "--pause", "output.wav"],
await execa({
cwd,
stdin: "ignore",
stdout: "inherit",
stderr: "inherit",
}).exited;
})`mpv --pause output.wav`;