- 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
secureFetchfrom@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!becomesHola), 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', soVOICE_NOTEtriggers still fire. With the defaultasText: '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 readsbody) 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:autoTranscribe.language, when set.- The language i18n resolved for the sender (
ctx.lang, base code:en-gbbecomesen), only when it comes from a real signal (stored preference, provider or detector), never the bare default. Turn this off withuseContextLanguage: false. - Otherwise none: the service detects the language.
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 carriesstream). A streamed reply must stay one editable text. - In-place edits from
ctx.edit. - Replies to a channel whose
capabilities.contentKindslacks'voice'.
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-*-transcribeand 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/baileystranscodeskind: 'voice'to OGG/Opus itself with anffmpegbinary (FFMPEG_PATH, the optionalffmpeg-staticpackage, orffmpegon 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 useoutputFormat: 'opus_48000_64'if your plan offers it. - This package does no transcoding (it does not depend on
ffmpeg).
Limits
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 extendsVoiceError 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
fetch and resolver to test them without
network. More in the
@botmesh/voice README.
Next steps
- AI plugin — answer transcribed voice notes with AI
- Streaming replies — why streamed output stays text
- Internationalization (i18n) — where
ctx.langcomes from - Messages & media —
ctx.voice()and receiving media