tools / text to speech

Text to speech

Turn any text into a spoken audio file, in this browser, with your own OpenRouter key. Written for the Journalist's news-style voice memos, but it reads whatever you give it.

What actually happens

The whole workflow, with nothing hidden — it is four steps and one HTTPS call.

  1. Your text stays here until you press the button. Nothing is sent while you type.
  2. The module loads on first usecore/sg-tts-openrouter from dev.tools.sgraph.ai, the same origin the transcription app imports its engine from. A visitor who only reads this page fetches none of it.
  3. One call to OpenRouter with your key, the chosen voice and the model openai/gpt-audio. Audio output must stream, and the only streamable format is 16-bit PCM at 24 kHz, so the module wraps the stream into a WAV in the page.
  4. The cost is read back, not guessed: the response carries a generation id, and the tool asks OpenRouter what that generation actually cost.

Whose money, whose data. The key is yours (sk-or-v1-…), kept in this browser under the same name the transcription app uses, sent only to OpenRouter, and billed to you — a short read costs a fraction of a penny. There is no backend here to store anything on. A long script may need splitting: audio models cap the length of a single call.

For agents: the JS API

The page publishes window.__tool — the same SgToolApi contract the transcription app uses — so a Playwright script (or any browser-driving agent) can make audio without a UI. Audio comes back as base64, because a Blob does not survive page.evaluate.

checking…

It is there before the page finishes loading. window.__tool is assigned while the page's module evaluates, so it does not wait for anything on the network — all seven actions work even if dev.tools.sgraph.ai is slow, blocked, or down, which a sandboxed browser allowed to reach only this page will find it is. The shared SgToolApi primitive then loads and takes over the same name with the same actions; you cannot tell from the calling side, and window.__toolStatus tells you anyway: { ready, mode: 'local' | 'sg-tool-api', engine: { loaded, error } }. It always exists and is never a console.warn you have to have been listening for.

Actionparamsreturns
synthesize{ text, voice?, model?, apiKey? } { base64, mime, bytes, durationMs, generationId, costUsd, voice, model }
getVoices{}{ voices, default, model }
setApiKey{ apiKey }{ saved } — stored in this browser only
hasApiKey{}{ present }
getLastAudio{}the last result, base64 included
saveLastAudio{ filename? }triggers a real download — Playwright can capture it
newsScriptFor{ post }{ script } — the news-read draft from an updates.json post
// Playwright: text in, .wav on disk, no UI involved.
await page.goto('https://voicedebrief.ai/tools/text-to-speech/')
await page.waitForFunction(() => window.__tool)        // present before load completes
console.log(await page.evaluate(() => window.__toolStatus))   // mode, and why, if you care
await page.evaluate(k => window.__tool.setApiKey({ apiKey: k }), process.env.OPENROUTER_KEY)

const r = await page.evaluate(() => window.__tool.synthesize({
  text: 'Here is the latest from VoiceDebrief…', voice: 'onyx' }))

console.log(r.durationMs, r.costUsd)                  // what it made, what it cost
await writeFile('memo.wav', Buffer.from(r.base64, 'base64'))

Machine-readable: manifest.json · SKILL__api.md · the page emits tts:done when a generation finishes, and tool:ready when the API publishes (once locally, again on upgrade) — though window.__toolStatus.ready is the durable fact, so you cannot miss it by attaching a listener too late.