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 chalk from "chalk"; import { execa } from "execa"; import { Window } from "happy-dom"; import wrapAnsi from "wrap-ansi"; const start = Date.now(); const { values: args } = parseArgs({ args: process.argv, options: { wait: { type: "boolean" }, w: { type: "boolean" }, url: { type: "string" }, u: { type: "string" }, }, strict: true, allowPositionals: true, }); const wait = args.wait ?? args.w; const url = args.url ?? args.u; let input = url ? url : await new Promise((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()); const window = new Window({ url: input.trim() }); const doc = window.document; doc.write(await result.text()); await window.happyDOM.waitUntilComplete(); const readable = new Readability(doc).parse(); if (readable?.textContent) input = readable.textContent; if (input.trim() === result.url) { console.error("Unable to read web page."); process.exit(1); } } if (!input) { console.error("No input given."); process.exit(1); } const home = os.homedir(); const cwd = `${home}/.ttsc/`; await execa({ cwd, shell: true, reject: false })`rm *.wav`; console.log(chalk.blue("Welcome to TTSC!")); console.log( chalk.yellow( "Beginning TTS processing... (This will take extra time the first time)", ), ); const koko = execa({ cwd, 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.catch(() => {}); koko.then(() => { const stop = Date.now(); const duration = ((stop - start) / 1000).toFixed(2); console.log(chalk.green(`\nProcessing finished: ${duration}s`)); }); } let first = true; const rl = createInterface({ input: koko.stdout, crlfDelay: Infinity }); for await (const line of rl) { const trimmed = line.trim(); if (!trimmed) continue; let info: { file: number; text: string }; try { info = JSON.parse(trimmed); } catch { console.log(trimmed); 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`)); } const [columns] = process.stdout.getWindowSize(); 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 execa({ cwd, stdin: "ignore", stdout: "inherit", stderr: "inherit", })`mpv ${file}`; } } await koko; 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...`)); try { await execa({ cwd, shell: true })`sox *.wav output.wav`; console.log(chalk.green(`Playing audio from ${cwd}output.wav`)); } 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 execa({ cwd, stdin: "ignore", stdout: "inherit", stderr: "inherit", })`mpv --pause output.wav`;