Дисклеймер
Всем привет, это мой первый пост, если вдруг будет интересно, продолжу писать на эту тематику. Я не являюсь опытным и профессиональным разработчиком, поэтому буду делиться тем, что узнал сам и по какому пути шел. Мой путь не является правильным да и пишу в первый раз, поэтому судите «строго»:‑)
Этап первый - появление задачи
В прошлом году на работе появилась задача для нашего отдела эксплуатации, где я работаю, о поиске сервиса, в котором можно подводить итоги совещания. Это в целом — стандартная для нас задача, а именно общение с вендором и определение, возможно ли их продукт внедрить в наш контур.
Пообщавшись с несколькими вендорами, в целом сложилась картина, что решения готовые есть, что их на рынке не мало, но что они используют, неизвестно. Собранные результаты я передал начальнику и начал ждать. Основное требование у всех вендоров было наличие сервера с GPU, что крайне не дешево, а так же, большинство из них, хоть и продают систему во внутренний контур, но при этом есть ограничение на кол‑во часов совещаний, что не сильно классно.
Спустя 2 месяца бюрократическая машина заработала, и начали что-то делать, но всё как-то заглохло, а люди, которым этот сервис был очень нужен, не переставали к нам ходить, поэтому появилась идея, а что есть в open source...
Этап второй - поиск open source
Пообщавшись с вендорами, я понял, что есть несколько этапов создания протокола совещания, а именно:
Диаризация — разбиение аудиофайла на сегменты по участникам (На самом деле опоционально, если есть агент для ВКС, но в случае с записью — необходим)
Транскрибация — превращение голоса в текст
Формирование отчета — создание из полученной транскрипции некого протокола
В целом это все основные этапы, которы нужны для этого. Ну и куда же можно пойти в 25 году для поиска? В GPT.) Знаю, знаю, плохо злоупотреблять моделями, но, блин, гуглить? Слишком лень, при том что я делал это в не рабочее время.
Быстро нашел модельку Whisper от open‑ai, поставил себе на ноут, записал короткую аудиозапись, проверил, в целом прикольно, работает.
Дальше задался вопросом о локальных LLM, начал с deepseek, но, как оказалось, дистиллированные модели — крайне не очень работают на русском, да и как дальше оказалось, итоги подводит так себе, поэтому глаз зацепился за llama3, потестил, в целом нормально, ее дальше и начал использовать.
Что по поводу диаризации — тут уже сложнее, потому что запустить её из терминала, не выходит, нужно писать код (в целом нужно написать код для нормальной работы, но это чуть дальше), а для просто результатов, что можно получить, перечисленного выше, хватает.
Ну я собрал результаты, скинул начальнику, он сказал: «Прикольно, а что по поводу целикового продукта?» Я сказал, что хз, нужно время. Он сказал, что ок, попробуй. Ну я и начал пробовать в свободное время, почему нет
Этап третий - первые попытки
Для начала, решил попробовать всё собрать на python (В целом вся логика на нем и осталась). Скачал либы, начал играться, и понял, что к перечисленным выше этапам, при работе с аудиофайлом, нужен еще один — конвертация. Для конвертации решил взять ffmpeg, просто и быстро. Сделал вот такую штуку:
ffmpeg
.input(mp4_file)
.filter("anlmdn")
.output(wav_file, format='wav', acodec='pcm_s16le', ac=1, ar='16k')
.run(quiet=True, overwrite_output=True)
Просто решил конвертировать, обрезать частоты, слегка подавить шум и все, готово, wav формат. Да, конвертирую до сих пор только с MP4, но мне пока что больше и не надо.
Конвертация готова, дальше диаризация, что делать с ней... Решил взять pyannote, показалась простой штукой, куда можно закинуть аудиофайл и получить сегменты в float, плюс слегка пострадав, удалось подключить CUDA что ускорило в целом все.
pipeline = SpeakerDiarization.from_pretrained(
"pyannote/speaker-diarization-3.1",
use_auth_token=settings.hf_token
)
pipeline.to(torch.device(device))
diarization = pipeline(wav_file_path)
for turn, _, speaker in diarization.itertracks(yield_label=True):
duration = turn.end - turn.start
if duration < 0.2:
continue
if speaker not in speaker_map:
speaker_map[speaker] = next_speaker_id
next_speaker_id += 1
segments.append((speaker_map[speaker], turn.start, turn.end))
Добавил фильтрацию по сегментам меньше 0.2 секунд, просто откидываю, потому что смысла в них нет. Вот и получился этап с диаризацией, классно, прикольно, двигаем дальше.
Следующий этап — транскрипция, там я уже потыкался с разными моделями large/medium/turbo, в целом очень понравилась турбо, быстрая и качественная, но есть нюансы в виде «Субтитры предоставил...», зато узнал, откуда взялся этот бред в голосовухах тг, сейчас правда не знаю, есть ли оно всё еще :-)
model = whisper.load_model("turbo", device=device)
result = model.transcribe(
segment_path,
language="ru",
beam_size=10,
temperature=cycle * 0.2,
)
text = result.get("text", "").strip()
В целом таких настроек мне хватило, и текст получался более менее норм, суть уловить можно, да и от косяков обезопасился костылями в виде if, они, конечно, всё равно проскакивают, но куда меньше.
Ну и дальше последний этап — это создание протокола, для этого решил взять ollama с llama3, (когда запускал на работе, взял llama3.3, т.к. оперативка позволяла не ждать пол года генерации, а всего пол часа) ну и сделал простого агента для обращения к нему.
message_payload = [
{'role': 'system', 'content': prompt},
{'role': 'user', 'content': message}
]
response: ChatResponse = chat(
model=settings.ollama_model,
messages=message_payload,
options={'num_ctx': int(settings.ollama_num_context)}
)
content = response.message.content.strip()
Добавил расширение контекста, потому что по умолчанию он 2к, а этого мало (но это сделал уже сильно позже, вначале не понимал, почему ответы становились хуже с размером сообщения).
Получил готовый черновой вариант (Большую часть кода упустил, потому что это же не урок, а просто описание моего пути) и начал его использовать на работе. Мне присылали видеозапись совещания, я запускал этого монстра, получал транскрипцию, отправлял назад, мне писали какой спикер — кто, я запускал запрос в LLM, получал отчет, отдавал назад.
Дебажил получившуюся штуковину, разбирался в LLM, узнал про ограничение контекста, узнал, начал делить совещание перед отправкой в LLM на сегменты по 10 минут совещания, дальше понял, что полученные итоги лучше прогнать еще раз, чтобы получить готовый протокол и так и работали месяца два. Да, на CPU запускать долго, зато бесплатно.:-)
Но история с закупкой как‑то затихла, движений никаких не было с вендорами, зато начальника заинтересовала моя разработка. Предложил продолжить эту историю с разработчиками в нашей команде, что в целом ок.
Пообщавшись с разработчиком, он решил прикрутить мое решение как доп модуль к существующему у них решению, что я так же одобряю, но это моя разработка, поэтому мне стало интересно сделать что‑то полностью своё.
Этап четвертый - создание полноценного приложения
В целом, данный этап идет до сих пор.
Я решил, что хочу подразобраться в различных технологиях, поэтому решил писать фронт на реакте с тайпскриптом и сделать некую прослойку, которая будет общаться с базой и ставить задачу в бэк. Для этого решил взять golang, потому что круто, модно, молодежно (Ну и есть друг го разраб)
Тут я столкнулся с серьезной проблемой — отсутствие опыта, из‑за которого я сильно пострадал на архитектуре, рефакторинге и т. п., но это уже история на следующий раз, который может быть, а может и не быть.
Всё что я пишу, выкладываю на gitHub, потому что код, написанный дома — твой код (вот ссылка, если вдруг интересно).
А вот вам картинка интерфейса который сейчас есть, так скажем, затравка на будущее, если вдруг кто‑то прочитает.

Всем кто дочитал — спасибо! :-)