Голосовой Telegram-бот состоит из понятной цепочки: получить файл, расшифровать аудио и вернуть текст в тот же чат. Мы используем python-telegram-bot и локальный faster-whisper. Модель работает на вашем компьютере, токен хранится в переменной окружения, а тяжёлая расшифровка уходит в отдельный поток, чтобы бот не зависал на каждом сообщении.
Пользователь видит одно действие: отправил голосовое и через несколько секунд получил текст. Внутри бот проходит несколько границ. Telegram присылает update с метаданными, отдельный запрос getFile выдаёт путь, библиотека скачивает OGG во временный каталог, а speech-to-text модель превращает звук в сегменты. Наконец Python склеивает сегменты и отвечает.
Фильтр VOICE направляет только голосовые сообщения в отдельный обработчик.
faster-whisper читает OGG локально на CPU и возвращает генератор сегментов.
Бот собирает текст, ограничивает длину сообщения и удаляет временный файл вместе с каталогом.
Как голосовое проходит через Telegram и Python
При старте приложение один раз создаёт WhisperModel small для CPU с int8. Это занимает память, зато модель не загружается для каждого сообщения. Application слушает обновления Telegram. Когда приходит voice, обработчик проверяет размер, скачивает файл во временный каталог и вызывает обычную функцию transcribe_sync через asyncio.to_thread.
| Этап | Что происходит | Признак результата |
|---|---|---|
| Update | Telegram сообщает file_id и длительность | Обработчик получил voice |
| Download | getFile скачивает OGG во временный каталог | Файл существует локально |
| STT | faster-whisper создаёт и перебирает сегменты | Получена строка |
| Reply | Бот возвращает текст и очищает временные данные | Ответ виден в том же чате |
У проекта есть один главный маршрут: получить входные данные, проверить их, выполнить действие и показать результат. Если каждый этап можно проверить отдельно, ошибка перестаёт быть загадкой.

От входных данных до видимого результата. Файл скачивается отдельно от update и удаляется после ответа
.env в Git и отзывайте через BotFather после утечки.Пишем обработчик и локальную расшифровку
Создайте проект и установите зависимости: uv init voice-bot, затем uv add python-telegram-bot faster-whisper. В актуальном faster-whisper PyAV декодирует аудио, поэтому отдельный системный FFmpeg для этого примера не требуется. Создайте бота через BotFather, задайте переменную BOT_TOKEN и запустите uv run python bot.py. На Windows PowerShell переменная для текущего окна задаётся как $env:BOT_TOKEN="...".
import asyncio
import os
import tempfile
from pathlib import Path
from faster_whisper import WhisperModel
from telegram import Update
from telegram.ext import Application, ContextTypes, MessageHandler, filters
model = WhisperModel("small", device="cpu", compute_type="int8")
def transcribe_sync(path: Path) -> str:
segments, _ = model.transcribe(str(path), language="ru", vad_filter=True)
return " ".join(segment.text.strip() for segment in segments).strip()
async def voice_to_text(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
message = update.effective_message
voice = message.voice
if voice.file_size and voice.file_size > 20 * 1024 * 1024:
await message.reply_text("Файл больше 20 МБ, пришлите запись короче.")
return
status = await message.reply_text("Скачиваю и расшифровываю...")
try:
with tempfile.TemporaryDirectory() as folder:
audio_path = Path(folder) / "voice.ogg"
telegram_file = await voice.get_file()
await telegram_file.download_to_drive(audio_path)
text = await asyncio.to_thread(transcribe_sync, audio_path)
await status.edit_text(text[:4000] or "Не удалось услышать речь.")
except Exception:
await status.edit_text("Не получилось обработать запись. Попробуйте ещё раз.")
token = os.environ["BOT_TOKEN"]
app = Application.builder().token(token).build()
app.add_handler(MessageHandler(filters.VOICE, voice_to_text))
app.run_polling(allowed_updates=Update.ALL_TYPES)- Бот отвечает на голосовое статусом, поэтому пользователь видит, что работа началась.
- OGG сохраняется только во временном каталоге и удаляется после выхода из блока with.
- Модель small работает локально на CPU с int8 и не требует платного API.
- Во время распознавания event loop остаётся свободным для других обновлений.
Функция transcribe_sync намеренно остаётся синхронной, а граница с async проходит в asyncio.to_thread. Так тяжёлая работа не блокирует цикл Telegram-бота, но код распознавания остаётся простым и тестируемым обычным вызовом.

Что отличает устойчивый проект от случайного успеха. CPU-задача не должна останавливать приём новых сообщений
Почему модель нельзя запускать прямо в async-цикле
Bot API не передаёт весь звук внутри update. Объект Voice содержит file_id, длительность и иногда размер. get_file получает File с временным file_path, после чего download_to_drive сохраняет данные. faster-whisper принимает путь и возвращает segments как ленивый генератор.
| Часть | Ответственность | Что проверить |
|---|---|---|
| MessageHandler | Отбирает только VOICE updates | Текстовые сообщения не вызывают модель |
| getFile | Готовит файл Telegram к скачиванию | Размер укладывается в лимит |
| TemporaryDirectory | Хранит OGG во время запроса | После ответа файл удалён |
| WhisperModel | Преобразует звук в сегменты | Генератор полностью перебран |
| to_thread | Выносит CPU-задачу из event loop | Бот принимает другие update |
Размер модели влияет на скорость, память и качество. tiny или base быстрее, small обычно даёт более устойчивую русскую речь, а крупные модели требуют больше ресурсов. Не обещайте мгновенный ответ: измерьте время на своём компьютере и ограничьте длительность входа.
Проверяем тишину, длинный файл и ошибку сети
Сначала отправьте короткую фразу без шума и сверьте текст. Затем пришлите две секунды тишины, голосовое на другом языке и запись рядом с музыкой. Для каждого случая бот должен дать понятный результат, а не зависнуть. Добавьте проверку duration и отклоняйте файлы длиннее выбранного лимита.
- Отправьте фразу из пяти слов и сравните ответ с оригиналом.
- Проверьте тишину и убедитесь, что бот не отправляет пустое сообщение.
- Задайте лимит длительности и получите понятный отказ для длинной записи.
- Запустите две расшифровки и посмотрите, отвечает ли бот на /start между ними.
- Уберите language="ru" и сравните автоопределение на двух языках.
- Перезапустите процесс без BOT_TOKEN и прочитайте ошибку окружения.
- Голосовое скачивается и удаляется после ответа.
- Расшифровка возвращается в тот же чат.
- Тишина и большой файл обработаны явно.
- Токен отсутствует в коде и репозитории.
Хороший голосовой бот заметно показывает состояние, ограничивает нагрузку и не оставляет аудио после обработки. Только после этого есть смысл добавлять резюме, тайм-коды или сохранение заметок.

Четыре проверки перед следующим шагом. Приватность и ограничения проверяются вместе с качеством
Делаем бота удобнее и безопаснее
Развить проект можно тремя безопасными способами. Первый: возвращать язык и длительность вместе с текстом. Второй: разбивать длинную расшифровку на сообщения по предложениям. Третий: по явной команде сохранять заметку пользователя в его собственном хранилище. Не добавляйте автоматическую пересылку и публикацию текста без подтверждения.
Как изучить тему в Кодике
В Кодике сначала соберите обычного эхо-бота, затем функцию, которая принимает путь и возвращает строку. Когда обе части работают отдельно, соедините их через временный файл и to_thread. Такой маршрут отделяет Telegram, файловую систему и модель друг от друга.
| Шаг | Что изучить в Кодике | Мини-проверка |
|---|---|---|
| 1 | Функции и строки Python | Склеить тестовые сегменты в одну фразу |
| 2 | Файлы и with | Создать и автоматически удалить временный каталог |
| 3 | async и обработчики | Ответить на update без блокировки |
| 4 | Модель speech-to-text | Расшифровать локальный OGG |
| 5 | Ограничения и очередь | Обработать тишину, размер и два запроса |
В тренажёре Кодика перепишите transcribe_sync и проверку лимита отдельно от Telegram. Затем восстановите обработчик по шагам: статус, скачивание, поток, ответ. Если вы можете назвать, какая строка отвечает за каждый этап, проект перестаёт быть непрозрачной связкой библиотек.
Секрет легко попадает в Git. Читайте BOT_TOKEN из окружения и сразу отзывайте утёкший токен.
faster-whisper возвращает генератор. Распознавание запускается во время итерации или list(segments).
CPU-задача блокирует event loop. Перенесите синхронную функцию через asyncio.to_thread.
Используйте TemporaryDirectory и не логируйте содержимое голосовых без явной причины и согласия.
Что получится в итоге

Бот показывает статус, скачивает OGG во временный каталог, расшифровывает его локально и возвращает готовый текст без сохранения аудио.
Почему transcribe вызывается через to_thread?
Распознавание нагружает CPU и иначе остановит async event loop, который принимает новые сообщения.
Когда faster-whisper начинает реальную работу?
Во время перебора ленивого генератора segments, а не только в момент получения этого объекта.
Нужен ли системный FFmpeg для этого примера?
В актуальном faster-whisper аудио декодирует PyAV, который включает нужные библиотеки. Сверяйте требования текущей версии.
Как понять, что проект действительно работает?
Проверьте основной сценарий, неверный ввод, повторный запуск и один граничный случай. Затем объясните путь данных своими словами.
Можно ли начать с готового кода из статьи?
Да. Добейтесь результата, измените одно правило и соберите ключевой файл заново без копирования.
Асинхронность, функции и файлы закрепите в курсе Python. Для похожей событийной архитектуры посмотрите Discord-бота на Python.
Локальные модели разберите в статье про Ollama, а хранение секретов и публикацию проекта свяжите с материалом про GitHub.