Whisper: как я дал своему агенту слух. Голос в текст без облаков и подписок

Всем привет!

Всё началось с агента. Я собрал себе помощника, который живёт в телеграме и делает рутинную работу. Об этом был предыдущий пост. И почти сразу выяснилась простая вещь: мне удобнее ему диктовать, чем печатать. Мысль быстрее сказать вслух, чем набирать пальцами, особенно когда идёшь куда-то или руки заняты.

Но тут загвоздка. Агент не слышит. Пока он не умеет превращать голос в текст, любое моё голосовое для него просто шум. Вот с этого всё и началось.

Дальше дорога одна из двух: отдать голос в чужое облако на расшифровку или поставить распознавание у себя. Облако я не взял. Голос это данные, часто личные, а на платформе, где у каждого клиента свой агент, чужие голосовые уходят пачками, и считать каждую минуту деньгами плохая затея. Значит, слух агенту я делаю свой, локальный. Ничего никуда не уходит, минуты не считаются, интернет вообще не нужен.

Так у меня появился Whisper. Дальше подробно, как я его выбрал, поставил и прикрутил к агенту, чтобы он наконец начал слышать.

Что такое Whisper и на что он опирается

Whisper это открытая модель распознавания речи от OpenAI. Она переводит звук в текст почти на сотне языков, знает пунктуацию и вполне сносно справляется с русским, если взять модель побольше.

Как она думает, если по-простому. Звук сначала приводят к одному стандарту: 16 кГц, моно, один поток. Дальше модель рубит запись на куски по 30 секунд и прогоняет каждый через энкодер, того, кто слушает и превращает шум в смысловые векторы. Потом декодер по шагам печатает текст. Звук нигде не сохраняется, между запусками модель вообще ничего не помнит. Для приватности это ровно то, что нужно.

На что опираться при выборе железа, тоже без магии. Упирается всё в две вещи: в процессор или видеокарту и в оперативную память. Модель целиком живёт в памяти, поэтому самой большой нужно около пяти гигабайт свободной RAM. Есть видеокарта NVIDIA, считает она, и считает в разы быстрее. Нет видеокарты, считает процессор, и это тоже рабочий вариант, просто медленнее.

И отдельная деталь, которая экономит кучу нервов: в Whisper есть фильтр тишины, VAD, определение голосовой активности. Он выкидывает паузы и шум до того, как модель вообще начнёт думать. Без него на длинной записи с паузами модель любит галлюцинировать и дописывает фразы, которых не было, обычно что-то вроде «Спасибо за просмотр».

Три движка: какой брать, а какой обходить стороной

Реализаций три, и я, как положено, сначала схватил не ту.

Первая, openai-whisper. Оригинал, ставится через pip и тянет за собой PyTorch. Работает, но на процессоре жрёт память и считает медленно. Нет видеокарты NVIDIA, это худший из выборов.

Вторая, faster-whisper. Та же модель, но пересобранная на движке CTranslate2 от SYSTRAN. Точность не меняется, а скорость выше в несколько раз, памяти меньше, и есть сжатие int8. Главное: PyTorch ей не нужен вообще, зависимости весят копейки. Я взял именно её, и дальше речь только о ней.

Третья, whisper.cpp. Чистый C++, без питона, отлично идёт на слабом железе и даже на Raspberry. Хороша, если питон не нужен в принципе. Минус один: подключать к своим скриптам менее удобно.

Логика простая. Есть NVIDIA или Apple, или просто хочется на питоне, берите faster-whisper. Нужен минимализм на слабом железе, берите whisper.cpp.

Размер модели: какую брать под свой сервер

Модель это главный выбор. Каждая ступень заметно умнее и заметно медленнее предыдущей. Вот мои ориентиры по весу на диске и по аппетиту к памяти.

Модель Вес Память Скорость Качество Кому
tiny ~75 МБ ~1 ГБ молниеносно слабое черновые наброски, разобрать пару слов
base ~145 МБ ~1 ГБ очень быстро так себе слабый сервер, простые короткие фразы
small ~480 МБ ~1,5 ГБ быстро уже читаемо ноутбук, короткие заметки
medium ~1,5 ГБ ~2,5 ГБ средне хорошее компромисс, если с памятью туго
large-v3 ~3 ГБ ~5 ГБ самая медленная лучшее есть видеокарта, нужна максимальная точность
large-v3-turbo ~1,6 ГБ ~5 ГБ быстрая для больших почти как large-v3 обычный сервер и русский
distil-large-v3 ~1,5 ГБ ~3 ГБ очень быстрая только английский записи только на английском

Я советую начинать с large-v3-turbo. Это отдельная модель, сжатая от large-v3: качество почти то же, а работает в разы быстрее. Для русского это сейчас золотая середина, и текст чистый, и на процессоре терпимо. Полную large-v3 берут, когда на первом месте точность, а скорость неважна, и обязательно с видеокартой. Distil берут только под английский, на других языках он ярко тупит.

Про варианты с суффиксом .en отдельно: они все только для английского и заметно слабее обычных на другом языке. Русскоязычным их брать не надо.

Железо: видеокарта или голый процессор

Видеокарта NVIDIA. Ставят pip install faster-whisper и библиотеки CUDA, в коде пишут device=»cuda» и compute_type=»float16″. Считает в 10, а то и в 20 раз быстрее процессора: час записи расшифровывается за пару минут. Нужна карта посвежее с подходящей версией CUDA и хотя бы четыре гигабайта видеопамяти под большую модель.

Видеокарта AMD. Возможно через ROCm, но это отдельная возня с драйверами, и работает не везде. Начинать отсюда я бы не советовал.

Встроенная графика Intel. Простого пути нет. Есть вариант через OpenVINO и отдельный конвертер модели, но это уже почти научная работа. Проще посчитать на процессоре.

Apple, M1 и новее. Есть отдельная связка mlx-whisper, считает на графическом ядре Mac быстро и тихо. Если у вас мак, смотрите в её сторону.

Только процессор, как у меня. Работает. Выставляем compute_type=»int8″: сжатие без потери качества на слух, зато вдвое меньше памяти. Число потоков ставим по числу ядер. И важный нюанс: старый процессор без поддержки AVX может тормозить катастрофически, так что на древнем железе лучше не жадничать и взять модель среднего размера.

Установка: от пустого сервера до первой расшифровки

Ставим в отдельное окружение, чтобы не гадить в системный питон. Первым делом сам питон и утилиту для окружений.

apt update
apt install -y python3 python3-venv ffmpeg

ffmpeg я всё равно ставлю, хотя faster-whisper читает звук своими силами, через библиотеку PyAV, которая приносит ffmpeg с собой. Он нужен для конвертации форматов, если понадобится.

Теперь окружение и библиотека.

python3 -m venv /opt/whisper/venv
/opt/whisper/venv/bin/pip install faster-whisper aiogram python-dotenv

aiogram и python-dotenv нужны только для телеграм-бота, который будет дальше. Если бот не нужен, ставьте один faster-whisper и получайте всё остальное.

Проверяем, что встало, и знакомимся с версиями.

/opt/whisper/venv/bin/python -c "import faster_whisper; print(faster_whisper.__version__)"
/opt/whisper/venv/bin/pip list | grep -E "faster|ctranslate"

Первая расшифровка скачает модель из интернета и положит её в кеш, в ~/.cache/huggingface. Модель большая, это одна загрузка на полтора гигабайта, дальше всё считается локально, интернет не нужен.

Если сервер закрытый и в интернет не ходит, модель приносят на флешке. Скачиваете папку с моделью на машине с интернетом.

pip install huggingface_hub
huggingface-cli download mobiuslabsgmbh/faster-whisper-large-v3-turbo --local-dir model-turbo

И дальше просто указываете путь к папке в коде вместо имени модели. Именно так сделано у меня: модель лежит в /opt/whisper/model-turbo, интернет ей не нужен.

Первый запуск: файл в текст

Кладём рядом файл whisper-cli.py. Это самый простой вход: даёшь ему файл, получаешь текст.

Код: whisper-cli.py
import sys

from faster_whisper import WhisperModel

model = WhisperModel("large-v3-turbo", device="cpu", compute_type="int8")


def main():
    segments, info = model.transcribe(sys.argv[1], language="ru", vad_filter=True)
    for s in segments:
        print(s.text.strip())


if __name__ == "__main__":
    main()

Запускаем так.

/opt/whisper/venv/bin/python whisper-cli.py запись.ogg

И получаем готовый текст. Вот что выдал мой сервер на тестовой записи в десять секунд, живой результат, не обещание.

Проверка распознавания речи на локальном сервере. «Игорь, привет! Сегодня 13 октября. Все считается на своем железе, в интернет ничего не уходит».

Ручки, которые я кручу

Когда базовый запуск заработал, начинается самое интересное. Вот главные параметры transcribe, которые я трогаю.

  • model, размер модели. Единственный по-настоящему важный параметр. Крупнее, значит точнее и медленнее.
  • language, например ru. Задать язык жёстко значит не тратить силы модели на определение и не промахиваться. Для русского я всегда ставлю ru. Оставляю language=None, если записи на разных языках: тогда она определит сама, и догадку видно в поле info.language.
  • beam_size, насколько широко перебирать варианты. По умолчанию 5, это разумно. Единица, жадный режим, быстрее, и на чистой речи почти не хуже. Больше пяти точнее, но заметно медленнее.
  • vad_filter=True, фильтр тишины. Держу включённым почти всегда: он и тишину выкидывает, и от галлюцинаций на паузах спасает.
  • initial_prompt, подсказка модели словами. Если в записи мат-термины, названия или аббревиатуры, которые она коверкает, перечислите их здесь, и качество на них вырастет. Например: «Хосткей, Caddy, rsync, DNSSEC».
  • condition_on_previous_text=False. Если модель зациклилась и повторяет одну фразу, этот флаг часто лечит. Правда, чуть теряется связность между кусками.
  • word_timestamps=True. Даёт время каждого слова, нужно для субтитров и для поиска в аудио по словам.
  • compute_type, как считать. int8 под процессор, float16 под NVIDIA. Ставить больше, чем нужно, смысла нет, это просто память на ветер.
  • cpu_threads. Число рабочих потоков, ставят по числу физических ядер. Больше ядер, чем есть, только хуже.

Сценарии: что этим делает мой агент

Сам я расшифровками не занимаюсь, у меня для этого есть агент. Поставить слух стоит один раз, а применений вагон. Вот живые сценарии, все на той же библиотеке.

Сценарий 1. Голосовое для агента. Самое главное. Я диктую агенту в телеграм, а он отвечает делом. Раньше пришлось бы печатать, теперь сказал вслух и пошёл дальше. Это тот самый случай, ради которого всё затевалось.

Сценарий 2. Разово, файл в текст. Тот самый whisper-cli.py выше. Даёшь ему путь, получаешь текст. Хоть запись с телефона, хоть аудио из сохранённых. Дописать удобный алиас w2t, и совсем хорошо.

Сценарий 3. Папка в текст, когда записей много. Руками каждый файл гонять глупо, поэтому есть скрипт, который проходит по папке и рядом с каждым аудио кладёт .txt.

Код: whisper-batch.py
import sys
from pathlib import Path

from faster_whisper import WhisperModel

model = WhisperModel("large-v3-turbo", device="cpu", compute_type="int8")
AUDIO = {".ogg", ".oga", ".mp3", ".wav", ".m4a", ".flac"}


def main():
    for path in sorted(Path(sys.argv[1]).iterdir()):
        if path.suffix.lower() not in AUDIO:
            continue
        segments, info = model.transcribe(str(path), language="ru", vad_filter=True)
        text = " ".join(s.text.strip() for s in segments)
        path.with_suffix(".txt").write_text(text, encoding="utf-8")
        print(f"{path.name}: готово")


if __name__ == "__main__":
    main()

Запуск простой, на выходе у каждой записи появляется текстовый файл с тем же именем.

/opt/whisper/venv/bin/python whisper-batch.py /mnt/records

Сценарий 4. Субтитры. Если нужны субтитры, а не просто текст, у сегментов есть начало и конец. Из них собирается обычный .srt, его можно подсунуть плееру или торрент-клиенту. Если нужны субтитры по словам, а не по фразам, добавляем в transcribe word_timestamps=True и собираем по словам.

Сценарий 5. Длинные записи: созвоны, лекции, интервью. Тут распознавание раскрывается по-настоящему, и агенту есть что разобрать и пересказать. Часовая запись на процессоре займёт примерно час, на видеокарте пару минут. Два совета. Обязательно vad_filter, длинные записи полны пауз. И второе: у больших файлов есть потолок по памяти, поэтому записи длиннее получаса лучше резать на куски. Резать можно так, не перекодируя лишний раз по кругу.

ffmpeg -i lecture.mp3 -f segment -segment_time 900 -c copy part_%03d.mp3

Сценарий 6. Свой сервер расшифровки для своих скриптов. Когда есть своё приложение или скрипт, которому нужна локальная расшифровка, поднимают небольшой сервер с интерфейсом, совместимым с OpenAI API. Тогда любой клиент, который умеет стучаться в OpenAI, стучится к вам по адресу http://127.0.0.1:8001/v1 и ничего не знает про интернет. Ниже полный рабочий сервер.

Свой сервер расшифровки: интерфейс как у OpenAI

Этот вариант у меня крутится на сервере как служба. Он даёт адрес /v1/audio/transcriptions, тот же формат, что у OpenAI, поэтому подходит любому клиенту. Модель грузится один раз и висит в памяти, поэтому ответы быстрые.

Код: whisper_server.py
import io
import os

from faster_whisper import WhisperModel
from fastapi import FastAPI, File, Form, UploadFile

model = WhisperModel(
    os.getenv("WHISPER_MODEL", "large-v3-turbo"),
    device="cpu",
    compute_type="int8",
)

app = FastAPI()


@app.post("/v1/audio/transcriptions")
async def transcribe(file: UploadFile = File(...), language: str = Form("ru")):
    data = await file.read()
    segments, info = model.transcribe(
        io.BytesIO(data), language=language, vad_filter=True
    )
    return {"text": "".join(s.text for s in segments).strip()}

Стартуем сервер.

/opt/whisper/venv/bin/pip install fastapi uvicorn
WHISPER_MODEL=/opt/whisper/model-turbo /opt/whisper/venv/bin/uvicorn whisper_server:app --host 127.0.0.1 --port 8001

Проверить его можно одной командой curl, отправив любой аудиофайл.

curl -sS -X POST http://127.0.0.1:8001/v1/audio/transcriptions \
  -F "file=@test.ogg" -F "language=ru"
# {"text":"Проверка распознавания речи на локальном сервере. «Игорь, привет! Сегодня 13 октября. Все считается на своем железе, в интернет ничего не уходит»."}

Чтобы сервер поднимался сам после перезагрузки, кладём его в systemd. Файл /etc/systemd/system/whisper-svc.service.

Файл: whisper-svc.service
[Unit]
Description=Локальный сервер распознавания речи (faster-whisper)
After=network.target

[Service]
WorkingDirectory=/opt/whisper
ExecStart=/opt/whisper/venv/bin/uvicorn whisper_server:app --host 127.0.0.1 --port 8001
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

И включаем.

systemctl daemon-reload
systemctl enable --now whisper-svc
systemctl status whisper-svc

Бот, который даёт агенту уши

Самое живое применение и тот случай, ради которого я всё это строил. Кидаешь боту голосовое, он отвечает текстом. Никакого облака, всё считается на моём сервере. Токен бота я держу в отдельном файле .env, а не в коде, потому что это секрет, и он не должен лежать в тексте скрипта. Вот мой .env.

Файл: .env
BOT_TOKEN=123456:ABC-DEF...

Токен берём у @BotFather: команда /newbot, пара вопросов, получаем строку вида 123456:ABC-DEF. Кладём её в .env, а сам файл закрываем от чужих.

chmod 600 .env

Вот сам бот.

Код: whisper-tg-bot.py
import asyncio
import os
import tempfile

from aiogram import Bot, Dispatcher, F
from aiogram.types import Message
from dotenv import load_dotenv
from faster_whisper import WhisperModel

load_dotenv()
model = WhisperModel("large-v3-turbo", device="cpu", compute_type="int8")
dp = Dispatcher()


def transcribe(path):
    segments, info = model.transcribe(path, language="ru", vad_filter=True)
    return "".join(s.text for s in segments).strip()


@dp.message(F.voice | F.audio | F.video_note)
async def on_media(message: Message, bot: Bot):
    media = message.voice or message.audio or message.video_note
    tmp = tempfile.NamedTemporaryFile(suffix=".ogg", delete=False)
    tmp.close()
    try:
        file = await bot.get_file(media.file_id)
        await bot.download(file, destination=tmp.name)
        text = await asyncio.to_thread(transcribe, tmp.name)
        await message.reply(text or "Не расслышал")
    finally:
        os.unlink(tmp.name)


async def main():
    bot = Bot(os.getenv("BOT_TOKEN"))
    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())

Что тут важно по устройству. Токен берётся из .env, в самом файле его нет. Модель грузится один раз при старте, дальше каждый запрос только считает. Расшифровка это тяжёлая синхронная операция, поэтому она уходит в отдельный поток, asyncio.to_thread, чтобы бот не замирал и мог принимать другие сообщения. Файл скачивается во временный и удаляется после ответа.

Запускаем, кидаем боту голосовое и смотрим, вернётся ли текст.

/opt/whisper-bot/venv/bin/python whisper-tg-bot.py

Чтобы бот работал постоянно, делаем ему службу. Токен она берёт из того же .env, а не из текста юнита. Файл /etc/systemd/system/whisper-bot.service.

Файл: whisper-bot.service
[Unit]
Description=Telegram-бот расшифровки голосовых (Whisper)
After=network.target

[Service]
WorkingDirectory=/opt/whisper-bot
EnvironmentFile=/opt/whisper-bot/.env
ExecStart=/opt/whisper-bot/venv/bin/python whisper-tg-bot.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Включаем и смотрим лог.

systemctl daemon-reload
systemctl enable --now whisper-bot
journalctl -u whisper-bot -f

Моё железо и живые цифры

Чтобы не быть голословным, покажу, на чём именно у меня это считалось и сколько заняло. Машина: Intel Core i7-6700, четыре ядра, восемь потоков, 64 гигабайта памяти, видеокарты нет, только встроенная графика Intel. Система Debian12, питон 3.11. Модель large-v3-turbo в сжатии int8, около 1,6 гигабайта на диске.

  • Загрузка модели в память при старте: около 11 секунд. Это плата один раз, при запуске, дальше модель уже в памяти.
  • Расшифровка записи на 7,4 секунды заняла около 8 секунд. Запись на 10,6 секунды заняла 9,2 секунды. Флаг beam_size=1 на тех же файлах дал почти то же время, то есть на коротких кусках экономить тут нечего.
  • То есть на моём процессоре скорость выходит примерно один к одному с длительностью записи. Минута голоса это примерно минута счёта. Долго, зато без единого рубля и без интернета.
  • Те же десять секунд через локальный сервер на порту 8001 пришли за 9,2 секунды, время то же самое, потому что считает тот же движок.
  • Определение языка само по себе сработало верно: русский с вероятностью 0,97.

Вывод по железу простой. Для коротких голосовых, на минуту-две, обычного процессора хватает с головой. Для длинных записей, лекций и созвонов, либо берите видеокарту NVIDIA, либо смиритесь, что час записи будет считаться час.

Грабли, на которые я наступал

  • Галлюцинации на тишине. Если в записи много пауз, модель может дописать то, чего не было. Лечу vad_filter=True, это первое, что включаю.
  • Забытый язык. Без явного language модель тратит лишнее время на определение и иногда промахивается. Для русских записей ставлю ru всегда.
  • Формат и частота. Модель ждёт 16 кГц моно. Библиотека сама переводит почти любой формат через PyAV, но если подсунуть битый файл, будет ошибка декодирования. Проверить файл можно через ffprobe.
  • Память на больших моделях. large-v3 кушает около пяти гигабайт. Если у сервера два гигабайта, он уйдёт в swap и будет считать вечность. Беру модель по железу.
  • Скорость на древнем процессоре. Без поддержки AVX всё будет очень грустно. Старый или слабый процессор это повод взять модель меньше, а не мучиться.
  • Потолок Telegram. Бот может скачать файл размером не больше 20 мегабайт. Длинные записи режу заранее или отдаю файлом поменьше.
  • Модель в интернете. Первый запуск тянет модель из сети. На закрытом сервере без интернета это упадёт с ошибкой, поэтому модель приношу заранее, как описано выше.
  • Пунктуация и регистр. Whisper пишет с заглавных и с точками, но имена и термины коверкает. Лечу initial_prompt со списком правильных слов.

Коротко о главном

Ставим faster-whisper в отдельное окружение, берём модель large-v3-turbo, считаем на процессоре в int8 или на видеокарте NVIDIA в float16. Дальше одна библиотека закрывает всё: голосовые для агента, разовые файлы, папки целиком, субтитры, длинные созвоны, свой сервер с интерфейсом OpenAI и телеграм-бота, который принимает голосовые и отдаёт текст.

Голос в текст без облаков это не роскошь и не сложно, это вечер работы. После него агент наконец слышит, а вы уже не вернётесь к платным минутам и чужим серверам.

Не прощаемся!

👁️ 18

Whisper: как я дал своему агенту слух. Голос в текст без облаков и подписок: 1 комментарий

  1. Важная оговорка по расшифровке. Сначала речь тестировал агент. Результат приложен его образца





Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

1 + 4 =

root@phoenix901:~# connect
[×]

Получай дайджест раз в неделю.
Без спама.