import fs from "node:fs/promises"; 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"; async function main() { const start = Date.now(); const { values: args } = parseArgs({ args: process.argv.slice(2), options: { wait: { type: "boolean" }, w: { type: "boolean" }, url: { type: "string" }, u: { type: "string" }, edit: { type: "boolean" }, e: { type: "boolean" }, }, strict: true, allowPositionals: false, }); const wait = args.wait ?? args.w; const url = args.url ?? args.u; const edit = args.edit ?? args.e; 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)); }); input = input.trim(); if (url !== undefined) { const result = await fetch(input); const window = new Window({ url: input }); 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.trim(); if (input === 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`; if (edit) { const file = await fs.open(`${cwd}input.txt`, "w+"); await file.writeFile(input); const editor = process.env.EDITOR || "nano"; await execa({ stdin: "inherit", stdout: "inherit", })`${editor} ${cwd}input.txt`; input = await file.readFile("utf8"); await file.close(); } 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, 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`; if (wait) { process.stdout.write("."); } else { console.log(`\n${chalk.blue(file)}\n${text}\n`); 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...\n`)); try { await execa({ cwd, shell: true })`sox *.wav 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(); } const [columns] = process.stdout.getWindowSize(); console.log(wrapAnsi(input, columns)); console.log(chalk.green(`Playing audio from ${cwd}output.wav`)); await execa({ cwd, stdin: "ignore", stdout: "inherit", stderr: "inherit", })`mpv --pause output.wav`; } main().catch((err) => { console.error(err); process.exit(1); });