import os from "node:os"; import process from "node:process"; 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"; const start = Date.now(); const { values: args } = parseArgs({ args: Bun.argv, options: { wait: { type: "boolean" }, w: { type: "boolean" }, url: { type: "boolean" }, u: { type: "boolean" }, }, strict: true, allowPositionals: true, }); const wait = args.wait ?? args.w; const url = args.url ?? args.u; let text = await Bun.stdin.text(); if (url) { const result = await fetch(text.trim()); const window = new Window({ url: text.trim() }); const doc = window.document; doc.write(await result.text()); await window.happyDOM.waitUntilComplete(); const readable = new Readability(doc).parse(); if (readable?.textContent) text = readable.textContent; if (text.trim() === result.url) { console.error("Unable to read web page."); process.exit(1); } } if (!text) { console.error("No input given."); process.exit(1); } const home = os.homedir(); const cwd = `${home}/.ttsc/`; $.cwd(cwd); await $`rm *.wav`.nothrow(); console.log(chalk.blue("Welcome to TTSC!")); console.log( chalk.yellow( "Beginning TTS processing... (This will take extra time the first time)", ), ); const koko = Bun.spawn({ cmd: ["uv", "run", "--", "python", "-u", "main.py"], cwd, stdin: Buffer.from(text), stdout: "pipe", stderr: "inherit", }); if (!wait) { koko.exited.then(() => { const stop = Date.now(); const duration = ((stop - start) / 1000).toFixed(2); console.log(chalk.green(`\nProcessing finished: ${duration}s`)); }); } let first = true; const decoder = new TextDecoder(); for await (const line of koko.stdout) { const decoded = decoder.decode(line).trim(); let infos: { file: number; text: string }[]; try { infos = Bun.JSONL.parse(decoded) as { file: number; text: string }[]; } catch { console.log(decoded); continue; } if (first) { first = false; const stop = Date.now(); const duration = ((stop - start) / 1000).toFixed(2); 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 file = `${home}/.ttsc/${info.file}.wav`; console.log(`\n${chalk.blue(file)}:\n${text}\n`); if (!wait) { await Bun.spawn({ cmd: ["mpv", file], cwd, stdin: "ignore", stdout: "inherit", stderr: "inherit", }).exited; } } } await koko.exited; if (!wait) process.exit(); const stop = Date.now(); 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`; console.log(chalk.green(`Playing audio from ${cwd}output.wav`)); } else { console.error("Sox not found. Unable to combine outputs."); console.log(chalk.yellow(`Audio available in ${cwd}`)); process.exit(); } await Bun.spawn({ cmd: ["mpv", "--pause", "output.wav"], cwd, stdin: "ignore", stdout: "inherit", stderr: "inherit", }).exited;