Skip to content

Commit 83332e0

Browse files
PathKnowerclaude
andcommitted
feat(transcription): add voice/video-note transcription
Adds transcribe_voice, a dual-engine transcription tool: Groq-hosted whisper-large-v3-turbo (default, leaves the server but preserves the full recording) and native Telegram Premium transcription (free, audio never leaves Telegram, but empirically drops the last speech segment in roughly 2 of 3 recordings, so it's kept as an explicit opt-in fallback rather than the default). Native results are polled while pending instead of returned as truncated text. Results are cached by (chat_id, message_id) in a dedicated SQLite file (0600/0700 permissions, mounted as its own Docker volume so it survives a rebuild) since Groq transcription is a real per-call cost, not free like native. get_history/get_messages/list_messages now surface cached transcripts inline instead of showing empty text for voice messages; TELEGRAM_TRANSCRIBE controls whether listings only show what's already cached (default) or also prefetch missing ones, bounded by a per-call budget so one large listing can't trigger dozens of downloads+uploads. Engine selection is configurable both ways: an explicit engine= argument on transcribe_voice, and a TELEGRAM_TRANSCRIBE_ENGINE default for callers that don't pass one. Every transcript is returned/rendered with an explicit note that it's a machine transcript, not a verbatim quote. transcribe_voice is read-only-annotated so it stays exposed under TELEGRAM_EXPOSED_TOOLS= read-only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 61b4f1c commit 83332e0

9 files changed

Lines changed: 1753 additions & 10 deletions

File tree

.env.example

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,36 @@ TELEGRAM_SESSION_NAME=telegram_session
8686
# --- Remembered contacts (optional) ---
8787
# TELEGRAM_ALIASES_FILE=/path/to/aliases.json
8888
# TELEGRAM_CONTACT_FUZZY=0 # exact-match aliases only
89+
90+
# --- Voice/video-note transcription (optional) ---
91+
# off: transcribe_voice tool disabled, listings never show transcripts.
92+
# on-demand (default): transcribe_voice works; get_history/get_messages/
93+
# list_messages fill in already-cached transcripts but never spend an API
94+
# call fetching a new one during a listing.
95+
# auto: also prefetches missing transcripts during a listing, bounded by
96+
# TELEGRAM_TRANSCRIBE_MAX_VOICES / _MAX_SECONDS per call.
97+
# TELEGRAM_TRANSCRIBE=on-demand
98+
#
99+
# Which engine transcribe_voice and auto-mode prefetch use by default (the
100+
# per-call engine= argument always overrides this):
101+
# - groq (default): Groq-hosted whisper-large-v3-turbo. Requires GROQ_API_KEY.
102+
# Downloads the voice note and sends it to Groq - leaves the server, not
103+
# free, but does not drop the recording's last words the way native
104+
# Telegram transcription does (proven 2026-08-20, see docs).
105+
# - telegram: native Telegram Premium transcription. Free, audio never
106+
# leaves Telegram, but empirically drops the last speech segment in
107+
# roughly 2 of 3 recordings. Requires Telegram Premium on the account.
108+
# Use for chats that should never be sent to a third party.
109+
# TELEGRAM_TRANSCRIBE_ENGINE=groq
110+
# GROQ_API_KEY=<groq api key, required when engine=groq is ever used>
111+
#
112+
# Per-call budget for TELEGRAM_TRANSCRIBE=auto prefetch (ignored otherwise;
113+
# transcribe_voice itself is never budget-limited since it's an explicit call).
114+
# TELEGRAM_TRANSCRIBE_MAX_VOICES=5
115+
# TELEGRAM_TRANSCRIBE_MAX_SECONDS=300
116+
#
117+
# Where the SQLite transcript cache lives. This is personal-chat text in
118+
# plaintext on disk - keep it on a mounted volume (see docker-compose.yml) and
119+
# in its own directory, not shared with backups of anything else. The file
120+
# gets mode 600; the directory 700.
121+
# TELEGRAM_TRANSCRIPT_CACHE_DIR=data/transcripts

README.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,24 @@ The server currently includes 80+ MCP tools grouped into these areas:
5757
When a reference is unknown, resembles one contact, matches several, or points at a contact that no longer resolves, tools send nothing and return a structured instruction telling the agent exactly what to ask you, to save the answer with `set_contact_alias`, and to retry once. `list_contact_aliases` shows one row per person with all their aliases (use it to spot a wrong memory), `delete_contact_alias` forgets one, and repointing an alias at someone else requires `replace=True`. The save path itself refuses a target it would have to guess at: contacts are saved by @username, phone, numeric ID, or an alias already confirmed for them.
5858

5959
Aliases live in `${XDG_STATE_HOME:-~/.local/state}/telegram-mcp/aliases.json` (owner-only, written atomically); `TELEGRAM_ALIASES_FILE` overrides the path, and a pre-existing `aliases.json` next to the code is still read as a fallback.
60-
- **Media:** send files, download media, upload files, send voice notes, stickers, GIFs, and inspect message media.
60+
- **Media:** send files, download media, upload files, send voice notes, stickers, GIFs, inspect message media, and transcribe voice messages/video notes (see below).
61+
62+
### Voice transcription
63+
64+
`transcribe_voice(chat_id, message_id, engine=None)` turns a voice message or video note into text. Two engines are available:
65+
66+
- `groq` (default): uploads the recording to Groq's hosted `whisper-large-v3-turbo`. Leaves the server and costs a download+upload per call, but doesn't drop the recording's last few words the way native transcription does. Requires `GROQ_API_KEY`.
67+
- `telegram`: native Telegram Premium transcription (`messages.TranscribeAudioRequest`). Free and never leaves Telegram, but empirically drops the last speech segment in roughly 2 of 3 recordings and requires Telegram Premium on the account. Long recordings come back `pending` and are polled automatically.
68+
69+
The engine is chosen per call via the `engine` argument, or otherwise defaults to `TELEGRAM_TRANSCRIBE_ENGINE` (`groq` or `telegram`). Results are cached by `(chat_id, message_id)` in a local SQLite file so repeat reads and repeat listings never re-transcribe the same message. Every transcript is returned with a `note` marking it as a machine transcript, not a verbatim quote — treat it as a paraphrase, not exact wording.
70+
71+
`get_history`, `get_messages`, and `list_messages` fill in already-cached transcripts for voice messages instead of leaving the text empty, controlled by `TELEGRAM_TRANSCRIBE`:
72+
73+
- `off`: `transcribe_voice` is disabled and listings never show transcripts.
74+
- `on-demand` (default): listings show cached transcripts but never spend an API call fetching a new one.
75+
- `auto`: listings also prefetch missing transcripts, bounded per call by `TELEGRAM_TRANSCRIBE_MAX_VOICES`/`TELEGRAM_TRANSCRIBE_MAX_SECONDS` (Groq isn't free, so this prefetch is budgeted rather than unbounded).
76+
77+
The cache lives in `TELEGRAM_TRANSCRIPT_CACHE_DIR` (default `data/transcripts`), written as a 700 directory / 600 file since it holds personal-chat text in plaintext — see [Docker](#docker) for why this needs its own volume mount in a container.
6178
- **Profile and privacy:** get your own account info, update profile fields, set or delete profile photos, inspect privacy settings, get user info/photos/status, and manage bot commands.
6279
- **Folders and drafts:** list, create, update, reorder, and delete Telegram folders; save, list, and clear drafts.
6380
- **Events:** wait for incoming messages with debounce (`wait_for_new_message`, `wait_for_settled_message`), optionally for one chat only via `chat_id` — without it any unrelated conversation wakes the wait — or enable the opt-in incoming event feed for callback-style delivery (see below).
@@ -161,6 +178,23 @@ normal authority inside the server process; read-only mode only prevents
161178
non-read-only tools from being registered and exposed through MCP. Accepted
162179
values are `all` (the default), `read-only`, and `read-only+<tool>,<tool>`.
163180

181+
Voice transcription (see [Voice transcription](#voice-transcription) above) is
182+
off by default in the sense that no transcript is ever fetched unless you ask
183+
for one — `transcribe_voice` is always available, and listings only pick up
184+
already-cached transcripts. Enable prefetching or pick an engine explicitly:
185+
186+
```env
187+
TELEGRAM_TRANSCRIBE=on-demand # off / on-demand (default) / auto
188+
TELEGRAM_TRANSCRIBE_ENGINE=groq # groq (default) or telegram
189+
GROQ_API_KEY=your_groq_api_key_here # required whenever engine=groq is used
190+
```
191+
192+
`engine=groq` requires `GROQ_API_KEY`; `engine=telegram` requires Telegram
193+
Premium on the account. `TELEGRAM_TRANSCRIBE_MAX_VOICES` (default 5) and
194+
`TELEGRAM_TRANSCRIBE_MAX_SECONDS` (default 300) bound how much `auto` mode
195+
prefetches per listing call; `TELEGRAM_TRANSCRIPT_CACHE_DIR` (default
196+
`data/transcripts`) sets where the SQLite cache is written.
197+
164198
Run the server locally:
165199

166200
```bash
@@ -498,6 +532,16 @@ The bundled Compose file runs the same setup:
498532
docker compose up --build -d
499533
```
500534

535+
It also mounts `./transcript_cache` into the container at
536+
`/app/data/transcripts` so the voice-transcription SQLite cache (see
537+
[Voice transcription](#voice-transcription)) survives a rebuild instead of
538+
living in the container's writable layer. Create it once, owned by the
539+
container's `appuser` (uid 1000), before starting:
540+
541+
```bash
542+
mkdir -p ./transcript_cache && chown 1000:1000 ./transcript_cache
543+
```
544+
501545
### One container per client (stdio)
502546

503547
Alternatively, an MCP client can spawn a dedicated container itself:

docker-compose.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,13 @@ services:
2323
# Replace './telegram_sessions' with your desired host path.
2424
# volumes:
2525
# - ./telegram_sessions:/app
26+
volumes:
27+
# Voice/video-note transcript cache (SQLite, see telegram_mcp/transcription.py).
28+
# Without this mount the cache lives only in the container's writable
29+
# layer and is lost on every `docker compose build`. It holds plaintext
30+
# personal-chat transcripts, so this directory should get its own
31+
# backup/retention policy rather than riding along with a general backup.
32+
# Host directory must be readable/writable by uid 1000 (appuser):
33+
# mkdir -p ./transcript_cache && chown 1000:1000 ./transcript_cache
34+
- ./transcript_cache:/app/data/transcripts
2635
restart: unless-stopped

telegram_mcp/runner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from telethon.errors import AuthKeyDuplicatedError
1111

1212
from telegram_mcp import runtime as _runtime
13+
from telegram_mcp import transcription as _transcription
1314
from telegram_mcp.runtime import *
1415
from telegram_mcp.singleton import (
1516
DEFAULT_GRACE_SECONDS,
@@ -191,6 +192,7 @@ async def _warm_caches() -> None:
191192
def main() -> None:
192193
_configure_allowed_roots_from_cli(sys.argv[1:])
193194
_runtime._apply_exposed_tools_mode()
195+
_transcription.validate_transcription_config()
194196
asyncio.run(_main())
195197

196198

0 commit comments

Comments
 (0)