
Voice Memo
- 1 installs
- 327 repo stars
- Updated May 25, 2026
- letta-ai/lettabot
Replies with voice memos using text-to-speech, sending native voice notes on Telegram and WhatsApp via a voice directive or CLI in silent mode.
About
Generates TTS voice memos and sends them as native voice bubbles across chat channels using a <voice> directive or lettabot-tts CLI. A developer or agent uses it when a short audio reply feels more natural than text.
- Renders as native OGG Opus voice bubbles on Telegram and WhatsApp
- Guidance on when not to use voice (code, URLs, long or structured content)
Voice Memo by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,983 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 16, 2026 (Skillselion catalog sync)
npx skills add https://github.com/letta-ai/lettabot --skill voice-memoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 327 |
| Last updated | May 25, 2026 |
| Repository | letta-ai/lettabot ↗ |
What it does
Replies with voice memos using text-to-speech, sending native voice notes on Telegram and WhatsApp via a voice directive or CLI in silent mode.
Files
Voice Memo Responses
Generate voice memos using TTS and send them as native voice notes.
Usage
Use the <voice> directive to send voice memos. No tool calls needed:
<actions>
<voice>Hey, here's a quick update on that thing we discussed.</voice>
</actions>With accompanying text:
<actions>
<voice>Here's the summary as audio.</voice>
</actions>
And here it is in text form too!Silent mode (heartbeats, cron)
For background tasks that need to send voice without a user message context:
OUTPUT=$(lettabot-tts "Your message here") || exit 1
lettabot-message send --file "$OUTPUT" --voiceWhen to Use Voice
- User sent a voice message and a voice reply feels natural
- User explicitly asks for a voice/audio response
- Short, conversational responses (voice is awkward for long technical content)
When NOT to Use Voice
- Code snippets, file paths, URLs, or structured data (these should be text)
- Long responses -- keep voice memos under ~30 seconds of speech
- When the user has indicated a preference for text
- When
ELEVENLABS_API_KEYis not set
Notes
- Audio format is OGG Opus, which renders as native voice bubbles on Telegram and WhatsApp
- Discord and Slack will show it as a playable audio attachment
- Use
cleanup="true"to delete the audio file after sending - The
data/outbound/directory is the default allowed path for send-file directives - The script uses
$LETTABOT_WORKING_DIRto output files to the correct directory - On Telegram, if the user has voice message privacy enabled (Telegram Premium), the bot falls back to sending as an audio file instead of a voice bubble. Users can allow voice messages via Settings > Privacy and Security > Voice Messages.
#!/usr/bin/env bash
# lettabot-tts - Generate speech audio via configurable TTS provider
#
# Usage: lettabot-tts <text> [output_path]
#
# Environment:
# TTS_PROVIDER - Optional. "elevenlabs" (default) or "openai".
#
# ElevenLabs:
# ELEVENLABS_API_KEY - Required. API key.
# ELEVENLABS_VOICE_ID - Optional. Voice ID (default: onwK4e9ZLuTAKqWW03F9).
# ELEVENLABS_MODEL_ID - Optional. Model ID (default: eleven_multilingual_v2).
#
# OpenAI:
# OPENAI_API_KEY - Required. API key.
# OPENAI_TTS_VOICE - Optional. Voice name (default: alloy).
# OPENAI_TTS_MODEL - Optional. Model (default: tts-1).
set -euo pipefail
TEXT="${1:?Usage: lettabot-tts <text> [output_path]}"
# The session subprocess CWD is set to workingDir (bot.ts:642), which is the
# same base directory that <send-file> directives resolve from. This means
# $(pwd) and LETTABOT_WORKING_DIR produce paths in the correct coordinate space.
OUTBOUND_DIR="${LETTABOT_WORKING_DIR:-$(pwd)}/data/outbound"
PROVIDER="${TTS_PROVIDER:-elevenlabs}"
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Error: Required command '$1' is not installed or not on PATH" >&2
exit 1
fi
}
preflight() {
require_cmd curl
require_cmd jq
}
preflight
# Ensure output directory exists
mkdir -p "$OUTBOUND_DIR"
# Use collision-safe random filenames when output path is not explicitly provided.
if [ -n "${2:-}" ]; then
OUTPUT="$2"
else
# Clean stale voice files older than 1 hour
find "$OUTBOUND_DIR" -name 'voice-*.ogg' -mmin +60 -delete 2>/dev/null || true
OUTPUT=$(mktemp "${OUTBOUND_DIR}/voice-XXXXXXXXXX.ogg")
fi
# ---------------------------------------------------------------------------
# Provider: ElevenLabs
# ---------------------------------------------------------------------------
tts_elevenlabs() {
if [ -z "${ELEVENLABS_API_KEY:-}" ]; then
echo "Error: ELEVENLABS_API_KEY is not set" >&2
exit 1
fi
local voice_id="${ELEVENLABS_VOICE_ID:-onwK4e9ZLuTAKqWW03F9}"
local model_id="${ELEVENLABS_MODEL_ID:-eleven_multilingual_v2}"
local http_code
http_code=$(curl -sS -w "%{http_code}" -o "$OUTPUT" \
"https://api.elevenlabs.io/v1/text-to-speech/${voice_id}" \
-H "xi-api-key: ${ELEVENLABS_API_KEY}" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg text "$TEXT" \
--arg model "$model_id" \
'{
text: $text,
model_id: $model,
output_format: "ogg_opus"
}'
)")
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "Error: ElevenLabs API returned HTTP $http_code (model=$model_id voice_id=$voice_id)" >&2
if [ -s "$OUTPUT" ]; then
echo "Error response preview:" >&2
head -c 2000 "$OUTPUT" >&2 || true
echo >&2
fi
rm -f "$OUTPUT"
exit 1
fi
if [ ! -s "$OUTPUT" ]; then
echo "Error: ElevenLabs TTS response was empty" >&2
rm -f "$OUTPUT"
exit 1
fi
}
# ---------------------------------------------------------------------------
# Provider: OpenAI
# ---------------------------------------------------------------------------
tts_openai() {
if [ -z "${OPENAI_API_KEY:-}" ]; then
echo "Error: OPENAI_API_KEY is not set" >&2
exit 1
fi
local voice="${OPENAI_TTS_VOICE:-alloy}"
local model="${OPENAI_TTS_MODEL:-tts-1}"
local http_code
http_code=$(curl -sS -w "%{http_code}" -o "$OUTPUT" \
"https://api.openai.com/v1/audio/speech" \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg text "$TEXT" \
--arg model "$model" \
--arg voice "$voice" \
'{
model: $model,
input: $text,
voice: $voice,
response_format: "opus"
}'
)")
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "Error: OpenAI TTS API returned HTTP $http_code (model=$model voice=$voice)" >&2
if [ -s "$OUTPUT" ]; then
echo "Error response preview:" >&2
head -c 2000 "$OUTPUT" >&2 || true
echo >&2
fi
rm -f "$OUTPUT"
exit 1
fi
if [ ! -s "$OUTPUT" ]; then
echo "Error: OpenAI TTS response was empty" >&2
rm -f "$OUTPUT"
exit 1
fi
}
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
case "$PROVIDER" in
elevenlabs) tts_elevenlabs ;;
openai) tts_openai ;;
*)
echo "Error: Unknown TTS_PROVIDER: $PROVIDER (supported: elevenlabs, openai)" >&2
exit 1
;;
esac
echo "$OUTPUT"