Switching language mid-conversation

VoiceThere voice sessions stay on one WebRTC connection while STT and TTS follow the caller's language. The runner swaps the listening and speaking models without tearing down the peer connection, and your agent keeps receiving transcripts as before.

Requirements

Automatic switching needs VoiceThere for both speech-to-text and text-to-speech (API id voicethere). Only the VoiceThere catalog maps a language to a speech model and a voice, so the runner can start the right pair on its own. With a cloud vendor on either side the dashboard keeps languageId.autoSwitch.enabled disabled, the API answers 400 with the code auto_switch_requires_voicethere_speech, and a value that was saved earlier is ignored at runtime (the runner logs language_switch.auto_unavailable).

Cloud vendors still work with switches your agent starts, through per-language profiles.

How a switch works

The caller usually keeps talking when the language is detected, so the runner stays quiet by default. It holds the caller's speech, starts the target speech services, and speaks only after the caller has finished.

Automatic language switch flowyesnonoyesyesnoLanguage detected(caller still speaking)Hold the caller's speechStart the target speech servicesInterrupt the caller?Wait message now(current voice, cannot be interrupted)Caller finishedTarget speech services ready?Wait message (current voice)unless already playedWait until readySwitch STT + voiceReplay the held speechSwitch took at leastminSwitchMs?Switch STT + voiceReady message (new voice)Replay the held speechSwitch STT + voiceReplay the held speechAnswer in the new languageAny step failsStay in the current language, apologise
Automatic language switch. Cyan outlines are decisions; the dashed box is the failure path.
SituationWhat the caller hears
Target not ready when the caller finishesA holding message in the current voice that names the target language, then the answer in the new language.
Target ready, switch took at least the ready-message thresholdThe ready message in the new voice, then the answer. Nothing is spoken while the caller still talks.
Target ready, switch faster than the thresholdThe answer in the new language, with no extra message.
waitMessage.mode is immediateThe wait message plays right away and cannot be interrupted, even if the caller is mid-sentence. The caller is cut off.

Automatic and manual switches

There are two ways a switch starts. In both, the runner swaps the STT and TTS models without dropping the WebRTC connection. Only the automatic switch sends voice_language_* events to the client.

SwitchWho decidesWait messageOff by default
Automatic (LID auto-switch)The runner, from spoken-language identification. See Spoken language identification.The runner plays one, in the language being left (settings below).Yes. Turn on with languageId.autoSwitch.enabled.
ManualYour agent, with setVoiceLanguage.None from the runner. The language-switch template speaks the same holding message; other agents say their own.Always available.

Agent API

import { defineAgent, setVoiceLanguage } from "@voicethere/agent";

export default defineAgent({
  onVoiceLanguageChanged({ sessionId, language }) {
    // Fired after an automatic switch (the runner's voice_language_changed event).
  },
  async onUserSpeechFinal({ sessionId }) {
    // Imperative switch (WebRTC stays up):
    await setVoiceLanguage(sessionId, { language: "de" });
  },
});

The voice-starter template skips calling setVoiceLanguage on every user_language event when deploy env has SHERPA_LID_AUTO_SWITCH enabled, because the runner owns STT and TTS in that mode.

Clients see the progress of an automatic switch as events on speech_event, listed under Client events. A manual switch with setVoiceLanguage sends none of the voice_language_* events; your agent knows when it switched and can tell the client itself.

Request shape

setVoiceLanguage and prepareVoiceLanguage in @voicethere/agent 0.10.0 take the same object, and it looks the same for every provider:

{
  language?: string,
  scope?: "stt" | "tts" | "both",
  sttVendor?: { provider, model?, language? },
  ttsVendor?: { provider, model?, voice?, language? },
}
FieldTypeMeaning
languagestringISO 639-1 code such as "de". The target language for any side you do not set explicitly.
scope"stt" | "tts" | "both"Which side changes. Defaults to the sides you pass; a bare language switches both.
sttVendor{ provider, model?, language? }Speech-to-text provider. It has no voice field. For voicethere, model is a catalog id such as en or en-small.
ttsVendor{ provider, model?, voice?, language? }Text-to-speech provider. For voicethere, voice or model is a catalog id such as en-lessac, and language picks a voice for that language.
voice, sttstring (deprecated)The older top-level voice and STT model. They still work on their own. Combined with the matching vendor object they fail with conflicting_fields.

STT providers: voicethere, openai, deepgram, assemblyai, google, elevenlabs, fishaudio, groq, azure, aws. TTS providers: voicethere, openai, elevenlabs, cartesia, fishaudio, google, deepgram, groq, azure, aws. The built-in speech is voicethere. The older ids local-sherpa and cluster-sherpa are deprecated aliases of it, accepted wherever a provider is accepted: the agent SDK, the API, the CLI and the dashboard.

What you can leave out

An omitted model, voice or language means that provider's default for vendor.language ?? language ?? the session's current language. For voicethere, model on the STT side is a catalog id (en, en-small), and voice or model on the TTS side is a catalog id (en-lessac). ttsVendor.language is accepted only for voicethere. A both switch to a language that has no VoiceThere STT model (Italian, Portuguese, Dutch, Polish, Hindi) changes TTS only.

Examples

VoiceThere on both sides, German:

import { setVoiceLanguage } from "@voicethere/agent";

await setVoiceLanguage(sessionId, {
  language: "de",
  sttVendor: { provider: "voicethere" },
  ttsVendor: { provider: "voicethere" },
});

Deepgram nova-3 for German speech-to-text, leaving TTS as it is:

await setVoiceLanguage(sessionId, {
  scope: "stt",
  sttVendor: { provider: "deepgram", model: "nova-3", language: "de" },
});

OpenAI tts-1 with the alloy voice, leaving STT as it is:

await setVoiceLanguage(sessionId, {
  scope: "tts",
  ttsVendor: { provider: "openai", model: "tts-1", voice: "alloy" },
});

VoiceThere speech-to-text with ElevenLabs text-to-speech:

await setVoiceLanguage(sessionId, {
  language: "de",
  sttVendor: { provider: "voicethere" },
  ttsVendor: { provider: "elevenlabs" },
});

prepareVoiceLanguage takes the same object and only starts the models, so the later setVoiceLanguage call is fast.

Failures and messages

A switch that cannot happen resolves { ok: false, reason, message }. The message says what to change. Where a message has two forms, the one for the side you passed is returned.

reasonmessage
invalid_requestsetVoiceLanguage options must be an object; language, scope, voice and stt are strings; sttVendor and ttsVendor are objects.
invalid_scopescope must be "stt", "tts" or "both".
missing_providersttVendor.provider is required. / ttsVendor.provider is required.
conflicting_fieldsPass the STT model as sttVendor.model, not stt, when sttVendor is set. / Pass the voice as ttsVendor.voice, not voice, when ttsVendor is set.
scope_mismatchscope "tts" cannot change sttVendor; use scope "both" or drop sttVendor. (and the mirror for scope "stt" with ttsVendor)
unsupported_fieldsttVendor has no voice field; set the voice with ttsVendor.voice. / ttsVendor.language only applies to provider "voicethere"; choose a voice for that language with ttsVendor.voice.
unknown_vendorUnknown provider "<id>". STT providers: <list>. TTS providers: <list>.
unsupported_vendor"<id>" cannot be used for stt. / "<id>" cannot be used for tts.
nothing_to_applyPass sttVendor, ttsVendor or language.
unknown_sttNo VoiceThere STT model matches sttVendor.model or the language; use a catalog id such as "en" or "en-small".
unknown_voiceNo VoiceThere voice matches ttsVendor.voice or the language; use a catalog id such as "en-lessac".
missing_credentialsAdd the AGENT_<VENDOR>_API_KEY project secret for provider "<id>". The message names the exact secret, for example AGENT_OPENAI_API_KEY. Azure names AGENT_AZURE_SPEECH_KEY and AGENT_AZURE_SPEECH_REGION; AWS names its AGENT_AWS_* secrets.
invalid_languagelanguage must be an ISO 639-1 code such as "en" or "de".
language_unavailableThe models for this language did not start; try again later or pick another language.
unavailableVoice switching is not available for this session.
timeoutThe voice switch did not finish in time; try again.

The agent log records a failure as Voice settings change failed: <reason>. <message> on the voice.settings_change_failed line, with message as a field. See System log events.

Deprecated fields

The top-level voice and stt strings still work when you pass them alone. Pass one together with the matching vendor object and the switch fails with conflicting_fields. New code should use ttsVendor.voice and sttVendor.model.

Per-language models

Only the STT and TTS models you picked for the project start warm. When a language switch needs another language, its models start on demand (a cold start). Each language can use its own models, and the table decides which ones.

In the dashboard, open Advanced voice pipeline and look under Language switch & presets. The Per-language models table has one row per language with an STT model and a TTS model, picked from the Sherpa catalog. Unknown model ids are rejected when you save. The table is stored in voice.profilesByLanguage, so the CLI and API take the same JSON.

LanguageSTT modelTTS model
German (de)dede-thorsten-high
Spanish (es)eses

A language without a row uses the catalog default for that language. voice.allowedLanguages limits which languages a switch may target. Leave it empty to allow the boot language plus every language in the table, or use * to allow any code for agent-driven switches.

A cloud vendor works as well: sttVendor, ttsVendor and voice can be set in the JSON (see STT & TTS vendors). The dashboard table shows those rows read-only.

voice.expectedLanguages is gone. It used to keep extra language pools warm for the whole life of a runner. Stored values are deleted and ignored.

What happens during a switch

The runner checks whether the target language is already running. If it is, the swap is instant and the caller notices nothing. If it is not, the runner starts the models, holds the caller's utterance, and speaks a holding message in the voice the call is using. The message names the target, for example “One moment, switching to German.” When it plays follows waitMessage.mode: with end_of_utterance the runner waits until the caller stops, immediate speaks at once, and off keeps it silent. Texts you set for a project can use the {language} placeholder. A healthy cold start takes roughly 4 to 6 seconds, and an occasional slow start can take longer. When the models are ready, the runner swaps, replays the held speech into the new STT, and the conversation goes on in the new language.

Each attempt waits 20 seconds. If the models are not running by then, the holding message plays again once the first one has finished, and the runner asks for the models once more and waits up to another 20 seconds. The second request is logged as language_switch.retry and sends no extra event to your agent. After about 40 seconds in total with no models, the runner sends voice_language_switch_failed with reason: "language_unavailable"and the call stays in the previous language. That event goes out before the held speech is given back to the previous language's STT, so your agent already knows that the next transcript comes from the old language model. When the wait message is on, the caller also hears a short sentence in the old voice, for example “Sorry, that language isn't available right now. Let's continue in English.” Built-in sentences exist for English, German, Spanish, French, Italian, Portuguese, Dutch and Polish. For any other language nothing is spoken, and you cannot change the text yet.

Held speech when a switch fails

CaseWhat happens to the held speech
The retry succeedsThe runner keeps buffering across both waits and replays the held speech into the new language, as in a switch that works the first time.
Both attempts failvoice_language_switch_failedis sent first, then the buffered speech goes to the previous language's STT. Nothing is dropped, but the transcript is in the previous language.

Two limits to know about:

  • Speech during the wait is only kept when the decision comes before the final. If language ID decides only after the final has arrived (for example with end_of_utterance timing), the runner replays that utterance but cannot buffer what the caller says during the wait. The language_switch.stt_hold log line then shows holdOutcome: "not_held" with reason: "final_already_seen".
  • immediate plays over the caller. An immediate wait message starts while the caller may still be talking, and their speech does not cut it off. If a session ever cannot play it that way, the message moves to the end of the utterance and the language_switch.wait_message log line carries reason: "immediate_unavailable".

Language check hold

With auto-switch on, the runner holds a user_speech_final until language ID has decided whether the utterance is in the session language, so your agent never answers a German sentence that the English STT transcribed. The hold is capped by languageId.autoSwitch.finalHoldMs (default 5000 ms). If language ID has not answered by then, the final is released as it is.

In practice most finals are not held at all. With languageId.timing at its default, early, the language is identified once while the caller is still speaking, as soon as languageId.minSpeechMs of speech is buffered (1500 ms by default). By the time the caller stops and the final arrives, the decision is already there.

Utterances shorter than minSpeechMs(a quick “yes” or “ok”) skip language ID entirely. They go to your agent immediately in the current language with no hold, the runner sends language_id_skipped instead of user_language, and the language of that utterance stays undetermined.

languageId.timingWhen the decision arrivesCPUUse it when
early (default)Once per utterance, as soon as minSpeechMs of speech is buffered, while the caller is still talking. Usually before the final.One identify pass per utterance.Almost always. Finals are rarely held.
end_of_utteranceOnce per utterance, after the caller stops talking.One identify pass per utterance.You want language ID to hear the whole utterance and can accept a short hold on each final.
continuousFirst after minSpeechMs, then again after each pass while the caller keeps talking.Several passes per long utterance. On a busy runner this competes with VoiceThere TTS and agent audio can get choppy; if it does, switch back to early.Callers who change language in the middle of long turns.

A lower minSpeechMs identifies sooner and lets fewer utterances skip the check, but Whisper tiny is less accurate on very short clips. A higher value is more accurate and lets more short replies through unchecked.

To guard against a false switch from one misheard clip, set languageId.autoSwitch.confirmUtterances to 2: the runner then switches only after two utterances in a row are identified as the new language.

Wait settings

All four settings live under languageId.autoSwitch and apply on the next deploy. They only matter when auto-switch is on.

SettingValuesDefaultEffect
waitAudiobuffer_replay, first_utterancebuffer_replayWhat the new STT hears after the swap. buffer_replay replays everything the caller said during the wait, first_utterance replays only the utterance that triggered language ID.
waitMessage.modeend_of_utterance, immediate, offend_of_utteranceWhen the message plays: after the caller stops talking, right away, or never.
waitMessage.skipWhenReadytrue, falsetrueNo message when the target models are already running.
waitMessage.textsJSON map of language code to textBuilt-in texts for en, de, es, fr, it, pt, nl, pl, ruThe text spoken for the language being left, up to 300 characters each, with that language's voice. {language} is replaced with the name of the language being switched to.

The text for a language says that the agent is switching away from it, so it is written in that language. If the caller leaves English for German, the English text plays with the English voice. {language}becomes the name of the target in the caller's current language, so the English text above says “German” and the German text says “Englisch” when the caller goes the other way. A language without a text falls back to the built-in text for it, and then to English. The message plays once per attempt, so at most twice when the first attempt times out.

Example timelines

The agent starts in English. The caller says, in German, “Guten Tag, ich möchte meine Bestellung ändern” (about 3 seconds) and then, still talking, “die Nummer ist 4711”. Times are illustrative and measured from the moment the caller starts talking.

Target language already running

TimeWhat happens
0.0 sCaller starts speaking German.
1.9 sLanguage ID answers de while the caller is still talking. The German models are running, so the swap is instant and voice_language_switching reports waitingForPools: false.
3.2 sThe held utterance is replayed into German STT. No wait message plays because skipWhenReady is on.
4.0 sYour agent receives the German final and replies in German.

Cold models, end_of_utterance (default)

TimeWhat happens
0.0 sCaller starts speaking German.
1.9 sLanguage ID answers de while the caller is still talking. The runner starts the German models and sends voice_language_switching with waitingForPools: true. The English final is held.
3.5 sCaller stops talking. The runner plays the English wait text with the English voice.
8.0 sGerman models are ready. The runner swaps and replays the held speech into German STT.
9.0 svoice_language_changed. Your agent receives the German final and replies in German.

Cold models, immediate

TimeWhat happens
0.0 sCaller starts speaking German.
1.9 sLanguage ID answers de while the caller is still talking. The German models start and the runner plays the English wait text right away, even though the caller is still talking. The caller hears the message over their own speech.
3.5 sCaller finishes. The wait message may still be playing, and the speech is buffered.
8.0 sGerman models are ready. Swap and replay.
9.0 sYour agent receives the German final.

Use immediate when silence feels worse than being talked over. If your runner does not yet support protected playback, the message plays when the caller stops talking, as in the end_of_utterance timeline.

Cold models, off

TimeWhat happens
0.0 sCaller starts speaking German.
1.9 sLanguage ID answers de while the caller is still talking. The German models start. Nothing is played, and the English final is held.
3.5 s to 8.0 sThe caller hears silence.
8.0 sGerman models are ready. Swap and replay.
9.0 sYour agent receives the German final and replies.

off fits agents that already tell the caller to hold on (a manual switch is one example) or setups where the client shows its own loading state from voice_language_switching.

Short utterance

TimeWhat happens
0.0 sCaller says “ja” (about 0.6 s).
0.6 sThe utterance is shorter than minSpeechMs (1500 ms), so language ID is skipped and the runner sends language_id_skipped.
0.8 sThe final goes to your agent immediately, with no hold, transcribed by the current language's STT. The language of this utterance stays undetermined and no switch happens.

early and end_of_utterance timing

Same German sentence (about 3 seconds), German models already running, minSpeechMs at its default of 1500 ms.

Timeearly (default)end_of_utterance
1.5 s1.5 s of speech is buffered. Identification runs once while the caller is still talking.Speech keeps buffering. Nothing is identified yet.
1.9 sLanguage ID answers de. The switch is decided. No further passes run for this utterance.
3.0 sCaller stops talking.Caller stops talking. Identification starts.
3.2 sThe final comes in. The decision is already there, so it is not held and goes straight to the replay into German STT.The final comes in and is held: language ID has not answered.
3.5 sLanguage ID answers de. Swap and replay start.

With continuous the first answer arrives at the same time as with early, and further passes keep running until the caller stops.

buffer_replay and first_utterance

The caller keeps talking while the German models start, so the two modes differ in what the new STT hears and what your agent receives. Cold models, end_of_utterance, swap at 8.0 s.

buffer_replay (default)first_utterance
Caller says during the wait“die Nummer ist 4711”“die Nummer ist 4711”
Replayed into German STT at 8.0 sThe first utterance and everything said after it, in order. The audio buffer is bounded, so on a very long wait the oldest audio is dropped.Only the first utterance.
Your agent receivesGerman finals for the first utterance and for the speech during the wait, as the STT finalizes them.One German final for the first utterance. The number is not replayed, so the caller has to repeat it.
Pick it whenCallers talk in one long stream and every word counts.You would rather drop speech from the wait than act on a half-heard request.

Client events

The runner sends these on the voice control channel as { type: "speech_event", event, ts, ...fields }. Fields with ? may be missing.

eventFieldsWhen
user_speech_final_heldutteranceId?, reason (language_check | language_switch), holdMsA final is held, waiting for language ID or for a switch to finish. holdMs is the most it will be held.
user_speech_final_releasedutteranceId?, heldMs, reason: same_language, lid_no_switch, lid_timeout, lid_skipped, replaced_by_replay, no_replay, switch_failed or switch_timeoutThe hold ended. replaced_by_replay means the agent gets the replayed final in the new language instead of this one.
language_id_skippedspeechMs, reason: too_short, deferred_tts, no_audio or undeterminedLanguage ID did not run or did not reach a decision for this utterance. The final goes on in the current language.
voice_language_switchinglanguage, from, to, waitingForPoolsAn automatic switch started. waitingForPools: true means the target models are starting.
stt_hold_startedholdMode, bufferedMsThe runner starts buffering inbound speech for the replay.
stt_hold_endedholdOutcome (for example released_replay, released_drop, cancelled, failed), bufferedMs, droppedMsBuffering ended. droppedMs is speech that was not replayed. Each hold produces one started and one ended event.
voice_language_changedlanguage, from, toThe automatic switch is done.
voice_language_switch_failedfrom, to, error, reason (language_unavailable or an apply error code), fallbackLanguage, attemptsThe session stays in fallbackLanguage. With reason: "language_unavailable" both attempts failed (attempts: 2) and error holds the underlying pool_timeout or pool_unavailable. Apply errors carry attempts: 1.

Manual switches with setVoiceLanguage send no voice_language_* events.

A UI can turn these into a small status line. A language check usually takes well under a second, so show it only if the hold lasts a little while. Call this handler with each speech_event message your client receives:

const names = new Intl.DisplayNames(["en"], { type: "language" });
let checkTimer: ReturnType<typeof setTimeout> | undefined;
let switching = false;

function handleSpeechEvent(msg) {
  switch (msg.event) {
    case "user_speech_final_held":
      if (msg.reason === "language_check") {
        checkTimer = setTimeout(() => setStatus("Checking language…"), 400);
      }
      break;
    case "voice_language_switching":
      clearTimeout(checkTimer);
      switching = true;
      setStatus(`Switching to ${names.of(msg.to)}…`); // "Switching to German…"
      break;
    case "user_speech_final_released":
      clearTimeout(checkTimer);
      if (!switching) setStatus(null); // a running switch keeps its status
      break;
    case "voice_language_changed":
      clearTimeout(checkTimer);
      switching = false;
      setStatus(null);
      break;
    case "voice_language_switch_failed":
      clearTimeout(checkTimer);
      switching = false;
      if (msg.reason === "language_unavailable") {
        setStatus(
          `${names.of(msg.to)} isn't available right now, continuing in ${names.of(msg.fallbackLanguage)}`,
        );
        setTimeout(() => setStatus(null), 5000);
      } else {
        setStatus(null);
      }
      break;
  }
}

Dashboard and CLI

In the dashboard, open the project Voice panel, then Advanced voice pipeline and Spoken language identification. The wait settings, the wait texts table and the per-language models table are there. Deploy to apply changes.

voicethere projects voice-advanced list
voicethere projects voice-advanced set languageId.autoSwitch.enabled true
voicethere projects voice-advanced set languageId.timing end_of_utterance
voicethere projects voice-advanced set languageId.autoSwitch.waitAudio first_utterance
voicethere projects voice-advanced set languageId.autoSwitch.waitMessage.mode immediate
voicethere projects voice-advanced set languageId.autoSwitch.waitMessage.skipWhenReady false
voicethere projects voice-advanced set languageId.autoSwitch.readyMessage.minSwitchMs 3000
voicethere projects voice-advanced set languageId.autoSwitch.waitMessage.texts '{"en":"One moment, switching to {language}.","de":"Einen Moment bitte, ich wechsle auf {language}."}'
voicethere projects voice-advanced set voice.profilesByLanguage '{"de":{"stt":"de","tts":"de-thorsten-high"},"es":{"stt":"es","tts":"es"}}'
voicethere projects voice-advanced set voice.allowedLanguages en,de,es
voicethere projects voice-advanced set languageId.autoSwitch.confirmUtterances 2

Other auto-switch settings

  • languageId.autoSwitch.enabled, default false. Requires languageId.enabled.
  • replayLastUtterance, finalHoldMs, minDwellMs, confirmUtterances and switchVoice tune stability and whether TTS follows STT. finalHoldMs is a safety cap (default 5000, max 10000): the runner holds a final until language ID decides and releases it after that many ms if LID has not answered.

Conversation history

When conversation history is enabled, each stored utterance includes an optional language field (ISO code from turn metadata). The dashboard Conversation timeline shows a language badge when present. Standalone user_language events are not stored as their own turns. Only finals and agent TTS text are persisted, with the language on the utterance row when known.

See Conversation history.

Observability

Every hold, switch and voice settings change also shows up in the session's agent logs as a line marked VoiceThere. See System log events for the full list with fields and example excerpts.

Session diagnostics and export bundles also carry runner logs with kind voice_language_auto_switch and voice_switch_wait_message (mode, played, duration and text length, not the text).

Wait and ready messages

Two short messages can mark a switch. The wait message covers a switch that is not finished when the caller stops talking. The ready message covers a switch that finished earlier but took long enough that a silent change would feel odd. Both are optional and use the project settings below. They apply to automatic switches. For switches your agent starts, the runner passes the same settings to the agent (getVoiceLanguageSwitchSettings).

SettingDefaultEffect
waitMessage.modeend_of_utteranceShown in the dashboard as "Interrupt the caller to announce the switch". end_of_utterance means no: wait until the caller stops. immediate means yes: play a protected message at once. off plays no wait message.
readyMessage.minSwitchMs2000Play the ready message when the switch took at least this many milliseconds. 0 turns it off. Range 0 to 15000.
readyMessage.textsbuilt-in textsJSON object of language code to text, up to 300 characters each. The text is spoken in the new voice.

Built-in ready messages:

LanguageText
enOkay, let's continue in English.
deOkay, machen wir auf Deutsch weiter.
esVale, sigamos en español.
frD'accord, continuons en français.
itVa bene, continuiamo in italiano.
ptCerto, vamos continuar em português.
nlOké, we gaan verder in het Nederlands.
plDobrze, kontynuujmy po polsku.
voicethere projects voice-advanced set languageId.autoSwitch.readyMessage.minSwitchMs 3000
voicethere projects voice-advanced set languageId.autoSwitch.readyMessage.texts '{"de":"Gut, dann auf Deutsch."}'

Switching from your agent

The runner does not switch for you when your agent calls the API. The language-switch template shows the same steps in agent code, built from small calls that work with any speech vendor:

  1. prepareVoiceLanguage(lang) starts the target speech services as soon as you know the language and says whether they are ready.
  2. When the caller has finished, say your own wait message if the target is not ready.
  3. setVoiceLanguage(lang) swaps STT and TTS. If the switch was slow, say a ready message in the new voice.
  4. replayLastUtterance()feeds the caller's last utterance to the new STT, then answer it.
  5. getVoiceLanguageSwitchSettings() returns the project settings above, so your agent follows the same wait and ready-message choices as the automatic flow.

Cold models and manual switches

The wait for cold models is bounded for switches your agent starts, too. setVoiceLanguagewaits up to 2 × 20 seconds, with one retry after the first 20, and then resolves { ok: false, reason: "language_unavailable" } instead of waiting on. prepareVoiceLanguage makes a single 20-second attempt and resolves ready: false with reason: "timeout" when the models are not up by then.

The language-switchtemplate handles both outcomes. It speaks the same holding message that names the target language, prepares once more after a timeout (and repeats the message), and tells the caller that the language isn't available when the second attempt fails or setVoiceLanguage reports language_unavailable. Customers get this template with the next @voicethere/agent release. Manual switches still send no voice_language_* events. The retry shows up in the system log as language_switch.retry with trigger: agent.

With a cloud vendor, map each language to a vendor and voice in voice.profilesByLanguage (sttVendor, ttsVendor, voice). Your agent logs each decision itself.

System log lines

Every decision of the automatic flow writes a language_switch.* line to the agent logs. Message texts you configured appear in the line; caller transcripts never do. Field details are in System log events.

LineFieldsMeaning
language_switch.decisiondecision (switch | same | no_switch | dwell | confirm_pending), from, to, confirmCount?, confirmNeeded?What the runner decided after language identification.
language_switch.stt_holdholdMode, holdOutcome, bufferedMs, droppedMs, reason?Whether the caller's speech was held while the new models start.
language_switch.startedfrom, to, trigger, poolsA switch began; pools says whether the target models were already running.
language_switch.poolsto, warm, poolsMs?The target models were already running (warm) or had to start.
language_switch.pools_readystt, tts, coldStartMsThe target models are running and how long that took.
language_switch.wait_messagemode, language, played, text?, reason?, repeat?The holding message was played in the old voice, or skipped with a reason. repeat is true for the second message, after the first attempt timed out.
language_switch.swapfrom, to, scope, durationMsSTT (and TTS when scope is both) now use the new language.
language_switch.ready_messagelanguage, played, reason?, durationMsThe ready message was played in the new voice, or skipped with a reason.
language_switch.replayok, ageMs?, reason?, via?The held speech was replayed into the new STT.
language_switch.committedfrom, to, durationMs, replayedThe session runs in the new language.
language_switch.retryfrom, to, attempt, reason (pool_timeout | pool_unavailable), trigger (lid | agent)The target models were not running after 20 seconds, so the runner asked again once. trigger is lid or agent (a setVoiceLanguage call). Level warn.
language_switch.failedfrom, to, reason, detail, fallbackLanguage, attemptsThe switch did not complete; the session stays in fallbackLanguage. Level warn.
language_switch.unavailable_messageplayed, reason? (disabled | no_text | no_host | speak_failed), fallbackLanguageThe built-in sentence that says the language is not available was played in the old voice, or skipped with a reason.
language_switch.final_heldreason, holdMsA transcript is held until the language is known.
language_switch.final_releasedreason, heldMsThe held transcript was released.
language_switch.prepareto, ready, reason?, durationMs, triggerThe agent called prepareVoiceLanguage; ready says if the target is running.
language_switch.auto_unavailablereason, stt, ttsAutomatic switching is stored as on but the project does not use VoiceThere for both STT and TTS.

← All documentation