Add text to speech with an integration
Connect the Azure Speech integration and use ctx.tts.azure.speak in Func to turn text into audio, with control over voice, rate, pitch and speaking style. upload stores the file in your project space and returns a permanent URL. The key stays on the server and your text is escaped into SSML for you.
Connect the Azure Speech integration and Func gets ctx.tts.azure.speak(): turn text into audio for article narration, podcasts, audiobooks or spoken prompts. The subscription key stays on the server and never appears in your code.
Connect Azure Speech
First create a Speech service resource in the Azure portal:
- Open Create Speech service and pick a subscription, resource group and region. The free F0 tier is enough to try it.
- Once created, go to Keys and Endpoint and copy KEY 1.
- Note the Location/Region, something like
eastus,eastasia,southeastasia.
Then open Backend → Integrations in the editor and pick Azure Speech:
| Field | What goes in | Required |
|---|---|---|
| Key | KEY 1 from the Keys and Endpoint page | Yes |
| Region | The short region id, e.g. eastus. Not a full URL | Yes |
| Default voice | Used when a call omits one, defaults to zh-CN-XiaoxiaoNeural | No |
| Audio format | Blank means mp3 | No |
| Channel tags | A project can hold several configurations (one per language, say); tags tell them apart | No |
Saving asks Azure for the voice list for real, so a wrong key or region fails right there.
speak: synthesise audio
The common case is just the text:
export function read(input, ctx) {
const audio = ctx.tts.azure.speak(input.text)
return { audioBase64: audio.audioBase64, mimeType: audio.mimeType }
}
Pass an object to control voice, rate and delivery:
export function read(input, ctx) {
const audio = ctx.tts.azure.speak({
text: input.text,
voice: 'en-US-JennyNeural',
rate: '+10%',
pitch: '-2st',
style: 'cheerful',
styleDegree: 1.5,
format: 'audio-24khz-48kbitrate-mono-mp3',
upload: true,
})
return { url: audio.fileUrl }
}
| Parameter | Notes |
|---|---|
text | Required. Plain text, not SSML, see why SSML is not accepted |
voice | Voice name; omit to use the integration's default |
rate | Speaking rate: +20%, -10%, 1.2, or words like slow / fast |
pitch | Pitch: +10%, -2st (semitones), high / low |
style | Speaking style such as cheerful, sad, newscast-casual. Only works on voices that support it |
styleDegree | Style intensity, usually between 0.01 and 2 |
language | Language tag such as en-US. Rarely needed, the voice carries it |
format | Output format, mp3 when blank, see audio formats |
upload | true stores the audio in your project space and returns fileUrl |
filename | Only used with upload. Defaults to a content hash |
The result:
| Field | Notes |
|---|---|
fileUrl | Present with upload: true. Ready for <audio src> |
audioBase64 | Present without upload. The raw audio, base64 encoded |
mimeType | e.g. audio/mpeg |
bytes | Size of the audio |
voice | The voice that was actually used |
format | The format that was actually used |
upload: true is usually what you want
Without upload the audio comes back into your code as base64. A minute of speech is roughly 500KB, close to 700KB once base64 encoded, which costs Func memory and then costs bandwidth again on the way to the browser.
With upload: true the audio goes straight into your project's own file space on the server and you get back a permanent URL:
const audio = ctx.tts.azure.speak({ text: paragraph, upload: true })
ctx.db.insert('chapters', { text: paragraph, audioUrl: audio.fileUrl })
Skip upload for one-off prompts you play immediately and never store.
Why only plain text, never SSML
The Azure API takes SSML. The platform builds it for you and escapes your text on the way in. You cannot write SSML tags yourself.
If you genuinely need to author whole SSML documents, the capability does not pass them through today. Configure your own key as an environment variable and call the API with fetch.
Audio formats
Blank means mp3, which covers almost everything. Common values:
| Format | Use |
|---|---|
audio-24khz-48kbitrate-mono-mp3 | The default. Small files, fine for the web |
audio-48khz-192kbitrate-mono-mp3 | Better quality, bigger files, good for audiobooks |
riff-24khz-16bit-mono-pcm | WAV, for further processing |
ogg-24khz-16bit-mono-opus | Opus, better quality per bit; check browser support yourself |
Common voices
| Voice | Notes |
|---|---|
zh-CN-XiaoxiaoNeural | Chinese, female. The default, supports several styles |
zh-CN-YunxiNeural | Chinese, male, younger sounding |
en-US-JennyNeural | English, female |
en-US-GuyNeural | English, male |
en-GB-SoniaNeural | British English, female |
ja-JP-NanamiNeural | Japanese, female |
The full list is in Azure Speech language and voice support. Not every voice supports style.
Several channels: via(tag)
A project can hold several configurations, one per language for instance, or a trial tier and a paid tier. Tag each one and pick it with via():
ctx.tts.azure.via('zh').speak({ text: '你好' })
ctx.tts.azure.via('en').speak({ text: 'hello' })
Without via() the configuration tagged default is used.
Caching and deduplication are yours
The platform does not cache: the same sentence twice costs twice. Keep your own table mapping a text hash to a URL.
export function speakCached(input, ctx) {
const key = input.text.trim()
const hit = ctx.db.query('tts_cache', { where: { text: key }, limit: 1 })
if (hit.list.length > 0) return { url: hit.list[0].url }
const audio = ctx.tts.azure.speak({ text: key, upload: true })
ctx.db.insert('tts_cache', { text: key, url: audio.fileUrl })
return { url: audio.fileUrl }
}
It is not built in because only you know what counts as the same sentence. Does a different voice count? A different rate? Building that judgement into the platform would only produce a version you disagree with.
Reading the errors
| Error | What to fix |
|---|---|
tts integration has no api key | The integration is not connected, or the key was cleared |
azure_tts_region is required | No region |
invalid azure_tts_region | A full URL or a display name with a space; it wants the short form eastus |
text is required | The text is empty |
invalid rate / invalid pitch / invalid style | The value failed the whitelist. See the accepted values above |
invalid voice | The voice name is malformed; it looks like en-US-JennyNeural |
Azure's own errors (out of quota, unknown voice, key and region mismatch) keep their message but come back as 400 or 502. An upstream 401 is never passed through as-is.
What this does not do
| Not included | Why |
|---|---|
| Raw SSML | The text usually comes from users, and not escaping it is an injection. See above |
| Caching and deduplication | Only your site knows what counts as the same sentence |
| Splitting long text | Where to break and whether to emit one file per chapter is a content decision, not an API one |
| Other TTS providers | Azure only for now. Others can be added behind the same interface when someone needs one |
See also: More integrations, OpenAI models, Func backend capabilities.
