#!/usr/bin/env node import events from "node:events"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import process from "node:process"; import readline from "node:readline"; import util 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"; class InputError extends Error { override name = "InputError"; } async function main() { const start = Date.now(); const { values: args, positionals } = util.parseArgs({ args: process.argv.slice(2), options: { wait: { type: "boolean" }, w: { type: "boolean" }, url: { type: "boolean" }, u: { type: "boolean" }, edit: { type: "boolean" }, e: { type: "boolean" }, }, strict: true, allowPositionals: true, }); const wait = args.wait ?? args.w; const url = args.url ?? args.u; const edit = args.edit ?? args.e; let input = positionals.join(" ").trim(); if (!input) { process.stdin.setEncoding("utf8"); process.stdin.on("data", (data) => { input += data; }); await events.once(process.stdin, "end"); } if (!input) { throw new InputError( "No input was given. Use positional arguments or stdin.", ); } if (url) { 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) { throw new InputError( "Unable to read webpage. This usually means it's blocking scrapers.", ); } input = readable.textContent.trim(); } const home = os.homedir(); const cwd = path.join(home, ".ttsc"); if (edit) { const file = path.join(cwd, "input.txt"); await fs.writeFile(file, input, "utf8"); const editor = process.env.EDITOR || "nano"; await execa({ cwd, stdin: "inherit", stdout: "inherit", stderr: "inherit", })`${editor} ${file}`; input = await fs.readFile(file, "utf8"); } console.log(chalk.blue("Welcome to TTSC!")); console.log( chalk.yellow( "Beginning TTS processing... (This will take extra time the first time)", ), ); const wavs = fs.glob(path.join(cwd, "*.wav")); for await (const wav of wavs) await fs.rm(wav); 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 files = []; const rl = readline.createInterface({ input: koko.stdout }); 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 file = path.join(cwd, `${info.file}.wav`); if (wait) { files.push(file); process.stdout.write("."); } else { const [columns] = process.stdout.getWindowSize(); const text = wrapAnsi(info.text, columns); console.log(`\n${chalk.blue(file)}\n${text}\n`); await execa({ cwd, stdin: "ignore", stdout: "inherit", stderr: "inherit", })`mpv ${file}`; } } await koko; if (!wait) return; 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 ${files} output.wav`; } catch (err) { if (err instanceof Object && "code" in err && err.code === "ENOENT") { console.error("Sox not found. Unable to combine outputs."); console.log(chalk.yellow(`Audio available in ${cwd}`)); return; } else { throw err; } } const [columns] = process.stdout.getWindowSize(); console.log(wrapAnsi(input, columns)); console.log( chalk.green(`Playing audio from ${path.join(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); });