diff --git a/README.md b/README.md index 6434db4..3ce0d9d 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Demos with a **Live** link run in your browser at [demos.speechify.ai](https://d | [`demos/ssml-emotion-tts/`](./demos/ssml-emotion-tts) | TypeScript (SDK) | | Drives emotion, pauses, prosody, emphasis, and pronunciation with SSML in a single POST /v1/audio/speech request. | | [`demos/web-audio-streaming/`](./demos/web-audio-streaming) | TypeScript (browser) | | Streams Speechify PCM into the browser Web Audio API for low-latency playback, behind a zero-dep proxy that keeps the key server-side. | | [`demos/deepgram-voice-agent-shim/`](./demos/deepgram-voice-agent-shim) | Go shim + Node | | Points Deepgram Voice Agent at the tts-shims OpenAI-compatible proxy so it speaks with a Speechify voice, key held server-side. | +| [`demos/speechify-tts-cli/`](./demos/speechify-tts-cli) | TypeScript (CLI) | | Generate speech from your terminal: pipe text or a file in, get an MP3 out, with the Speechify API. Voice, model, and format flags included. | | [`demos/vapi-custom-voice/`](./demos/vapi-custom-voice) | Go shim | | Points Vapi custom voice at the tts-shims Vapi-compatible proxy so it speaks with Speechify raw PCM, key held server-side. | | [`demos/pipecat-agent-speechify/`](./demos/pipecat-agent-speechify) | Python (Pipecat) | | Real-time voice pipeline using Pipecat with Deepgram STT, Anthropic Claude, and Speechify TTS through SpeechifyTTSService. | | [`demos/mastra-agent-speechify/`](./demos/mastra-agent-speechify) | TypeScript (Mastra) | | Text-in, speech-out Mastra Agent using an OpenAI LLM for replies and Speechify's simba-3.2 model for TTS via `@mastra/voice-speechify`. | diff --git a/demos/speechify-tts-cli/.env.example b/demos/speechify-tts-cli/.env.example new file mode 100644 index 0000000..534cec4 --- /dev/null +++ b/demos/speechify-tts-cli/.env.example @@ -0,0 +1 @@ +SPEECHIFY_API_KEY=your_api_key_here diff --git a/demos/speechify-tts-cli/README.md b/demos/speechify-tts-cli/README.md new file mode 100644 index 0000000..92a4535 --- /dev/null +++ b/demos/speechify-tts-cli/README.md @@ -0,0 +1,40 @@ +# Text-to-speech CLI + +Generate speech from your terminal with the Speechify API — pipe text or a file +in, get an MP3 out. Pairs with the Speechify post *"Text-to-speech from your +terminal: a CLI demo with the Speechify API"*. + +## What you get + +A single-file Node CLI that calls the Speechify API and writes the audio: + +- reads text from `--text`, `--file`, or stdin +- pick the `--voice`, `--model`, and `--format` +- writes the audio to `--output` (or raw bytes to stdout to pipe downstream) + +Login-free, zero prompt, drop it in a shell alias or a build step. + +## Run it yourself + +```bash +cp .env.example .env # paste your key into .env +npm install + +# from a string +npm start -- --text "Hello from your terminal" -o hello.mp3 + +# from a file +npm start -- --file chapter.txt -v lily --model simba-english -o chapter.mp3 + +# from stdin +cat notes.txt | npm start -- -o notes.mp3 +``` + +Prerequisites: Node 20+, a Speechify API key from +[platform.speechify.ai/api-keys](https://platform.speechify.ai/api-keys). + +## Where the code came from + +Uses the official `@speechify/api` client — the same `client.audio.speech()` +call used across the [demos repo](../). See the Speechify docs for voice ids and +available `audio_format` values. diff --git a/demos/speechify-tts-cli/demo.json b/demos/speechify-tts-cli/demo.json new file mode 100644 index 0000000..394b7d4 --- /dev/null +++ b/demos/speechify-tts-cli/demo.json @@ -0,0 +1,6 @@ +{ + "order": 100, + "title": "Text-to-speech CLI", + "stack": "TypeScript (CLI)", + "blurb": "Generate speech from your terminal: pipe text or a file in, get an MP3 out, with the Speechify API. Voice, model, and format flags included." +} diff --git a/demos/speechify-tts-cli/package.json b/demos/speechify-tts-cli/package.json new file mode 100644 index 0000000..c1a1119 --- /dev/null +++ b/demos/speechify-tts-cli/package.json @@ -0,0 +1,23 @@ +{ + "name": "speechify-tts-cli", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Text-to-speech from your terminal with the Speechify API", + "bin": { + "speechify-tts": "src/index.ts" + }, + "scripts": { + "start": "tsx src/index.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@speechify/api": "^3.0.1", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0" + } +} diff --git a/demos/speechify-tts-cli/src/index.ts b/demos/speechify-tts-cli/src/index.ts new file mode 100644 index 0000000..c79bf20 --- /dev/null +++ b/demos/speechify-tts-cli/src/index.ts @@ -0,0 +1,146 @@ +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { SpeechifyClient, SpeechifyError } from "@speechify/api"; + +type Args = { + text?: string; + file?: string; + voice: string; + model: string; + format: string; + output?: string; +}; + +const VOICES_HINT = + "george, henry, lily, matilda, simba-english, and any voice_id from platform.speechify.ai"; + +function parseArgs(argv: string[]): Args { + const out: Args = { voice: "george", model: "simba-english", format: "mp3" }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + const next = () => { + const v = argv[++i]; + if (v === undefined) throw new Error(`Missing value for ${a}`); + return v; + }; + switch (a) { + case "--text": + case "-t": + out.text = next(); + break; + case "--file": + case "-f": + out.file = next(); + break; + case "--voice": + case "-v": + out.voice = next(); + break; + case "--model": + case "-m": + out.model = next(); + break; + case "--format": + out.format = next(); + break; + case "--output": + case "-o": + out.output = next(); + break; + case "--help": + case "-h": + printHelp(); + process.exit(0); + default: + throw new Error(`Unknown argument: ${a}\nRun with --help for usage.`); + } + } + return out; +} + +function printHelp() { + console.log(`speechify-tts — text-to-speech from your terminal + +Usage: + speechify-tts --text "Hello world" -o hello.mp3 + echo "Read this aloud" | speechify-tts -o out.mp3 + speechify-tts --file chapter.txt -v lily -m simba-english + +Options: + -t, --text Text to synthesize (ignored if --file given) + -f, --file Read input text from a file + --voice Voice id (default: george) + --model Model id (default: simba-english) + --format Output format: mp3, ogg, wav, etc. (default: mp3) + -o, --output Write the audio file (default: stdout, raw bytes) + -h, --help Show this help + +Voices include: ${VOICES_HINT} +Requires SPEECHIFY_API_KEY in the environment (copy .env.example to .env).`); +} + +async function readStdin(): Promise { + const chunks: Buffer[] = []; + for await (const c of process.stdin) chunks.push(c as Buffer); + return Buffer.concat(chunks).toString("utf8"); +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + if (!args.file && !args.text && !process.stdin.isTTY) { + args.text = await readStdin(); + } + if (!args.file && !args.text) { + printHelp(); + process.exit(1); + } + + const token = process.env.SPEECHIFY_API_KEY; + if (!token) { + throw new Error("Set SPEECHIFY_API_KEY (copy .env.example to .env)."); + } + + const input = args.file + ? fs.readFileSync(path.resolve(args.file), "utf8") + : (args.text as string); + + const client = new SpeechifyClient({ token }); + + try { + const response = await client.audio.speech({ + input, + voice_id: args.voice, + audio_format: args.format as "mp3" | "ogg" | "wav" | "aac" | "pcm", + model: args.model as + | "simba-english" + | "simba-multilingual" + | "simba-3.0" + | "simba-3.2", + }); + + const audio = Buffer.from(response.audio_data, "base64"); + + if (args.output) { + fs.writeFileSync(path.resolve(args.output), audio); + console.error( + `Wrote ${args.output} (${audio.length.toLocaleString()} bytes, ` + + `${response.billable_characters_count} billable characters).`, + ); + } else { + process.stdout.write(audio); + } + } catch (err) { + if (err instanceof SpeechifyError) { + console.error(err.message); + process.exit(1); + } + throw err; + } +} + +main().catch((err) => { + console.error(err instanceof Error ? err.message : err); + process.exit(1); +}); diff --git a/demos/speechify-tts-cli/tsconfig.json b/demos/speechify-tts-cli/tsconfig.json new file mode 100644 index 0000000..eeb5226 --- /dev/null +++ b/demos/speechify-tts-cli/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/site/public/index.html b/site/public/index.html index ae37691..5ebae36 100644 --- a/site/public/index.html +++ b/site/public/index.html @@ -276,7 +276,7 @@ - + @@ -368,7 +368,7 @@

FAQ