What actually happens
The whole workflow, with nothing hidden — it is four steps and one HTTPS call.
- Your text stays here until you press the button. Nothing is sent while you type.
- The module loads on first use —
core/sg-tts-openrouterfromdev.tools.sgraph.ai, the same origin the transcription app imports its engine from. A visitor who only reads this page fetches none of it. - 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. - 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.
| Action | params | returns |
|---|---|---|
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'))