Your OS probably comes with TTS, mac has say, windows has SAPI, and linux has espeak or festival.
But they all suck at uttering compelext technical topics as would be the case of an Agent output.
I built this zero dependency CLI tool to leverage OpenAI's TTS API for high-quality speech synthesis.
- Supports all OpenAI voices (
nova,shimmer,echo,onyx,fable,alloy,ash,sage,coral). - Adjustable speech rate.
- Input from command line, file, or stdin.
- Save to file or play directly using
afplay.
- Ensure you have Node.js installed (v20.12+ for dynamic
process.loadEnvFile). - Set your
OPENAI_API_KEYin a.envfile or your environment. - Make the script executable:
chmod +x sayy.
./sayy "Hello, how are you today?"
./sayy -v alloy -r 1.2 "This is a faster voice."
cat text.txt | ./sayyPut the it in your PATH for global access (example)
ln -s ./sayy /usr/local/bin/sayyVerify and torture it with a complex, multi-line briefing:
sayy <<'EOF'
### Deployment Summary for `v2.4.0-rc1`
I have completed the audit of the following artifacts:
- `src/middleware/auth_v3.ts`: Optimized the JWT verification loop.
- `pkg/db/pool.go`: Fixed a connection leak during high-concurrency spikes.
- `deploy/k8s/service.yaml`: Updated resource limits for the `api-gateway` pod.
EOFThen you can ask you Agents to use somewhere in AGENTS.md:
- When you're done, brief me with the `sayy` command.#!/usr/bin/env node
//@ts-check
import { parseArgs } from "node:util";
import { join } from "node:path";
import { unlink } from "node:fs/promises";
import { spawnSync } from "node:child_process";
import { tmpdir } from "node:os";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
const MODEL = "gpt-4o-mini-tts";
function printHelp() {
console.log(`
Usage: sayy [options] [text]
Options:
-v, --voice <voice> Specify the voice (nova, shimmer, echo, onyx, fable, alloy, ash, sage or coral)
-r, --rate <rate> Speed of the speech (0.25 to 4.0, default: 1.0)
-f, --file <path> Read text from the specified file
-o, --output <path> Save audio to a file instead of playing it
-h, --help Show this help message
`);
}
// 1. Load .env or ../.env
const localEnv = join(import.meta.dirname, ".env");
const parentEnv = join(import.meta.dirname, "../.env");
try {
if (existsSync(localEnv)) process.loadEnvFile(localEnv);
else if (existsSync(parentEnv)) process.loadEnvFile(parentEnv);
} catch (e) {
/* ignore */
}
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
// 2. Parse Args
const { values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
voice: { type: "string", short: "v", default: "shimmer" },
rate: { type: "string", short: "r", default: "1.0" },
file: { type: "string", short: "f" },
output: { type: "string", short: "o" },
help: { type: "boolean", short: "h" },
},
allowPositionals: true,
});
if (values.help) {
printHelp();
process.exit(0);
}
if (!OPENAI_API_KEY) {
console.error("Error: OPENAI_API_KEY not found in .env");
process.exit(1);
}
async function main() {
try {
let text = positionals.join(" ");
// Handle file input
if (values.file) {
try {
text = readFileSync(values.file, "utf8");
} catch (e) {
console.error(`Error reading file: ${values.file}`);
process.exit(1);
}
} else if (!text && !process.stdin.isTTY) {
try {
text = readFileSync(process.stdin.fd, "utf8");
} catch (e) {
console.error("Error reading from stdin");
}
}
if (!text?.trim()) {
printHelp();
return;
}
// 3. Fetch Audio
const response = await fetch("https://api.openai.com/v1/audio/speech", {
method: "POST",
headers: {
Authorization: `Bearer ${OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: MODEL,
input: text,
voice: values.voice,
speed: parseFloat(values.rate),
response_format: "mp3",
}),
});
if (!response.ok) {
const err = await response.text();
throw new Error(`API Error: ${response.status} ${err}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
// 4. Output or Play
if (values.output) {
writeFileSync(values.output, buffer);
} else {
const tempFile = join(tmpdir(), `sayy-${Date.now()}.mp3`);
writeFileSync(tempFile, buffer);
spawnSync("afplay", [tempFile], { stdio: "inherit" });
unlink(tempFile).catch(() => {});
}
} catch (error) {
console.error(error);
process.exit(1);
}
}
main();For type safety (optional)
{
"name": "say",
"type": "module",
"private": true,
"devDependencies": {
"@types/node": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}