Skip to main content
  • STT: OpenAI (whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe, and compatible servers) and Deepgram (nova-3).
  • TTS: OpenAI (gpt-4o-mini-tts, tts-1, tts-1-hd) and ElevenLabs.
  • No SDKs: plain HTTPS through secureFetch from @botmesh/http (SSRF guard, timeouts, size caps).
  • install() is synchronous and never touches the network: adapters are resolved on first use, and a non-fatal warmup logs a bad config (missing API key, unknown provider) at startup.

Quickstart

autoTranscribe without stt, or autoReplyWithVoice without tts, throws a VoiceConfigError when the plugin is installed.

Transcribing voice notes (autoTranscribe)

A before('message') hook (priority 100, so it runs before most hooks) downloads each voice note, transcribes it and sets:
  • ctx.transcript: the text exactly as the service returned it ('' when nothing was said).
  • ctx.body: the same text without leading and trailing punctuation (¡Hola! becomes Hola), so equality triggers, c.ask(), NLU and AI handlers “hear” the audio.
  • ctx.media: the downloaded audio (it is not downloaded again).
  • ctx.transcriptError: the error when transcription failed. The message is still routed (without a transcript) and a warning is logged.
  • ctx.type: stays 'voice', so VOICE_NOTE triggers still fire. With the default asText: 'auto' it becomes 'text' only while a conversation of that user waits for text (c.ask(), c.waitText()), so a voice note answers the question. When the conversation waits for audio (c.waitMedia(['voice'])), a menu (c.choose(), which reads body) or any message (c.waitAny()), the note arrives as 'voice' with its transcript.
autoTranscribe: false disables the hook; ctx.tools.voice.transcribe(ctx) is still available on demand.

With the AI autoresponder

The @botmesh/ai autoresponder only answers text messages. With the default asText: 'auto', a voice note sent outside a conversation stays 'voice', so the AI does not answer it. To let the AI answer voice notes, set asText: true:

Language and i18n

The language hint for the STT service is, in order:
  1. autoTranscribe.language, when set.
  2. The language i18n resolved for the sender (ctx.lang, base code: en-gb becomes en), only when it comes from a real signal (stored preference, provider or detector), never the bare default. Turn this off with useContextLanguage: false.
  3. Otherwise none: the service detects the language.
After the hook writes the transcript into ctx.body, the core re-resolves the language when it came from the detector or the default, so an i18n detect function sees the spoken text. A ctx.lang set by a hook is kept.

Answering with voice (autoReplyWithVoice)

While a chat’s latest inbound message is a voice note, outbound text to that chat is synthesized and sent as a voice note instead. It is a before('send') hook, so it covers ctx.reply, flow say steps, c.say and the AI autoresponder alike. When the user writes text again, the chat goes back to text. These always stay text:
  • Everything ctx.stream() outputs: drafts, live edits, sentence chunks and the final message (the send payload carries stream). A streamed reply must stay one editable text.
  • In-place edits from ctx.edit.
  • Replies to a channel whose capabilities.contentKinds lacks 'voice'.
If synthesis fails, the original text is sent.

ctx.tools.voice

The plugin injects a VoiceClient as ctx.tools.voice (typed through module augmentation, no casts needed): Transcription options: language, prompt, signal, timeoutMs. Synthesis options: voice, instructions, signal, timeoutMs. A per-call timeoutMs can only tighten limits.timeoutMs. source may also be a Uint8Array.

Adapters

Every built-in adapter also accepts timeoutMs (it can only tighten limits.timeoutMs), maxResponseBytes and, for self-hosted servers on a trusted network, allowHttp / allowPrivateHosts (for example a Whisper-compatible server on http://127.0.0.1:8000/v1). You can also pass your own adapter (a local model, another service):

Audio formats

  • Input: WhatsApp and Telegram voice notes arrive as OGG/Opus, which Whisper, gpt-4o-*-transcribe and Deepgram accept directly. When the channel reports no mimetype, it is sniffed from the first bytes (OGG, WAV, MP3, MP4/M4A, WebM, FLAC, AMR), else OGG is assumed.
  • Output on WhatsApp: a voice note must be OGG/Opus. @botmesh/baileys transcodes kind: 'voice' to OGG/Opus itself with an ffmpeg binary (FFMPEG_PATH, the optional ffmpeg-static package, or ffmpeg on the PATH), so any format works. For the Cloud API and Telegram, ask the TTS for OGG/Opus: it is the OpenAI default (format: 'opus'); on ElevenLabs use outputFormat: 'opus_48000_64' if your plan offers it.
  • This package does no transcoding (it does not depend on ffmpeg).

Limits

The duration is checked before paying for a transcription when it is known (an explicit durationSec, or an OGG file). Other containers are not estimated. Limits are validated when the plugin is plugged (or the VoiceClient is built): timeoutMs must be between 1 and 2147483647 ms (the setTimeout range), and the other caps must be positive (Infinity turns a cap off). Anything else throws a VoiceConfigError naming the field.

Errors

Every error extends VoiceError and carries a stable code: Error messages and logs never include API keys, audio or transcripts. The service’s own error detail is kept (it explains a 400 or a 401), but the request’s API key and anything key-shaped (sk-… or sk_…, even masked) are redacted from it first.

Testing

The HTTP adapters accept injected fetch and resolver to test them without network. More in the @botmesh/voice README.

Next steps