IntegrationsAdd text to speech with an integration

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:

  1. Open Create Speech service and pick a subscription, resource group and region. The free F0 tier is enough to try it.
  2. Once created, go to Keys and Endpoint and copy KEY 1.
  3. Note the Location/Region, something like eastus, eastasia, southeastasia.

Then open Backend → Integrations in the editor and pick Azure Speech:

FieldWhat goes inRequired
KeyKEY 1 from the Keys and Endpoint pageYes
RegionThe short region id, e.g. eastus. Not a full URLYes
Default voiceUsed when a call omits one, defaults to zh-CN-XiaoxiaoNeuralNo
Audio formatBlank means mp3No
Channel tagsA project can hold several configurations (one per language, say); tags tell them apartNo

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 }
}
ParameterNotes
textRequired. Plain text, not SSML, see why SSML is not accepted
voiceVoice name; omit to use the integration's default
rateSpeaking rate: +20%, -10%, 1.2, or words like slow / fast
pitchPitch: +10%, -2st (semitones), high / low
styleSpeaking style such as cheerful, sad, newscast-casual. Only works on voices that support it
styleDegreeStyle intensity, usually between 0.01 and 2
languageLanguage tag such as en-US. Rarely needed, the voice carries it
formatOutput format, mp3 when blank, see audio formats
uploadtrue stores the audio in your project space and returns fileUrl
filenameOnly used with upload. Defaults to a content hash

The result:

FieldNotes
fileUrlPresent with upload: true. Ready for <audio src>
audioBase64Present without upload. The raw audio, base64 encoded
mimeTypee.g. audio/mpeg
bytesSize of the audio
voiceThe voice that was actually used
formatThe 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:

FormatUse
audio-24khz-48kbitrate-mono-mp3The default. Small files, fine for the web
audio-48khz-192kbitrate-mono-mp3Better quality, bigger files, good for audiobooks
riff-24khz-16bit-mono-pcmWAV, for further processing
ogg-24khz-16bit-mono-opusOpus, better quality per bit; check browser support yourself

Common voices

VoiceNotes
zh-CN-XiaoxiaoNeuralChinese, female. The default, supports several styles
zh-CN-YunxiNeuralChinese, male, younger sounding
en-US-JennyNeuralEnglish, female
en-US-GuyNeuralEnglish, male
en-GB-SoniaNeuralBritish English, female
ja-JP-NanamiNeuralJapanese, 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

ErrorWhat to fix
tts integration has no api keyThe integration is not connected, or the key was cleared
azure_tts_region is requiredNo region
invalid azure_tts_regionA full URL or a display name with a space; it wants the short form eastus
text is requiredThe text is empty
invalid rate / invalid pitch / invalid styleThe value failed the whitelist. See the accepted values above
invalid voiceThe 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 includedWhy
Raw SSMLThe text usually comes from users, and not escaping it is an injection. See above
Caching and deduplicationOnly your site knows what counts as the same sentence
Splitting long textWhere to break and whether to emit one file per chapter is a content decision, not an API one
Other TTS providersAzure only for now. Others can be added behind the same interface when someone needs one

See also: More integrations, OpenAI models, Func backend capabilities.

Render diagnostics