Comments 12
Чанки по 25 секунд, а как потом склеивать разорванные на полуслове предложения?
Зачем тут телеграм? Если надо читать голосовухи которые там ходят то вроде сберовский бот с ними справляется @smartspeech_sber_bot А длинные надо как то обрабатывать (сразу закидывать в какую то нейросеть которая всё умеет, типа джемини), что толку с огромной записи голоса, не будешь же ты это реально всё читать.
Я тоже пробовал телеграмм бота создать, получилось. Но решил его в программу сконвертировать для windows , чтобы файлы оставались локально, для безопасности. В итоге получилась практичная программулина, в которую можно закидывать хоть по гигабайт аудиофайлов запятая и оно всё расшифрует и сделает протокол совещания. Так и назвал: Программа протокола совещания.
Вы это сообщение с помощью своей программы транслировали? А то у вас "запятая" текстом написана)
Прикольно. Нейронка локальная или расшифровка через облако? В опенсорсе есть она? Интересно было бы посмотреть ее.
Подскажите, сколько видео памяти надо чтобы запустить модель? Ну или железо на котором запускали
На данный момент мой бот запущен на сервере с 3-х ядерным процессором и 4 гб оперативной памяти, то есть без гпу. Можете перейти в бота @saytotextbot и оценить скорость работы. Недавно отправлял аудио на 21 минуту, обработал за 2 минуты.
Скорость 10х?
У меня на 4 ядрах и 16гб получилось меньше 2х.
Там 2 модели, более быстрая совсем плохо распознает. Медленная более менее нормально.
import os
import time
import warnings
import tempfile
import subprocess
import io
import torch
import soundfile as sf
from dotenv import load_dotenv
from transformers import AutoModel
from pyannote.audio import Pipeline
# --- 1. НАСТРОЙКА ОКРУЖЕНИЯ ---
load_dotenv()
os.environ['HF_TOKEN'] = os.getenv("HF_TOKEN", "")
# proxy_url = "socks5h://10.8.1.8:1080"
# os.environ['HTTP_PROXY'] = proxy_url
# os.environ['HTTPS_PROXY'] = proxy_url
# os.environ['ALL_PROXY'] = proxy_url
# warnings.filterwarnings("ignore", category=UserWarning)
INPUT_AUDIO = r'C:\Users\user\Downloads\samples for ai\аудио\кусок радио-т подкаста несколько голосов.mp3'
MODEL_REVISION = "e2e_rnnt"
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def format_timestamp(seconds):
"""Превращает секунды в формат [HH:MM:SS]"""
return time.strftime('[%H:%M:%S]', time.gmtime(seconds))
def run_pipeline():
total_start = time.perf_counter()
if not os.environ.get('HF_TOKEN'):
raise ValueError("HF_TOKEN не найден в .env файле!")
# --- ШАГ 1: Загрузка GigaAM-v3 ---
print(f"--- Загрузка GigaAM-v3 ({MODEL_REVISION}) на {DEVICE}... ---")
model = AutoModel.from_pretrained(
"ai-sage/GigaAM-v3",
revision=MODEL_REVISION,
trust_remote_code=True
)
model.to(DEVICE).eval()
# --- ШАГ 2: Декодирование аудио в RAM ---
print("--- Декодирование аудио через FFmpeg... ---")
command = [
'ffmpeg', '-y', '-i', INPUT_AUDIO,
'-f', 'wav', '-ar', '16000', '-ac', '1', 'pipe:1'
]
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout_data, _ = process.communicate()
audio_buffer = io.BytesIO(stdout_data)
waveform_np, sr = sf.read(audio_buffer)
# Подготовка тензора (1, Samples)
waveform = torch.from_numpy(waveform_np).float().unsqueeze(0)
audio_duration = waveform.shape[1] / sr
# --- ШАГ 3: Диаризация (Учет версии 4.x) ---
print("--- Запуск Pyannote Diarization... ---")
pipeline = Pipeline.from_pretrained(
"pyannote/speaker-diarization-3.1",
token=os.environ['HF_TOKEN']
)
pipeline.to(DEVICE)
# Получаем результат
res = pipeline({"waveform": waveform, "sample_rate": sr})
# ГЛАВНОЕ ИСПРАВЛЕНИЕ ДЛЯ ВЕРСИИ 4.x:
# В новых версиях Annotation лежит в атрибуте .speaker_diarization
if hasattr(res, 'speaker_diarization'):
annotation = res.speaker_diarization
elif hasattr(res, 'annotation'):
annotation = res.annotation
else:
annotation = res
# Проверка: получили ли мы объект, по которому можно итерироваться
if not hasattr(annotation, 'itertracks'):
raise AttributeError(f"Не удалось извлечь итератор из объекта {type(res)}")
# Склеиваем идущие подряд реплики одного спикера
refined_segments = []
tracks = list(annotation.itertracks(yield_label=True))
if not tracks:
print("Речь не обнаружена.")
return ""
# Инициализация первого сегмента
curr_start = tracks[0][0].start
curr_end = tracks[0][0].end
curr_spk = tracks[0][2]
for turn, _, speaker in tracks[1:]:
# Если тот же спикер и пауза меньше 0.8 сек — объединяем
if speaker == curr_spk and (turn.start - curr_end) < 0.8:
curr_end = turn.end
else:
refined_segments.append((curr_start, curr_end, curr_spk))
curr_start, curr_end, curr_spk = turn.start, turn.end, speaker
refined_segments.append((curr_start, curr_end, curr_spk))
# --- ШАГ 4: Распознавание каждого сегмента через GigaAM ---
print(f"--- Распознавание {len(refined_segments)} сегментов спикеров ---")
final_output = []
for i, (start, end, speaker) in enumerate(refined_segments):
duration = end - start
if duration < 0.4: continue
start_sample = int(start * sr)
end_sample = int(end * sr)
chunk = waveform[:, start_sample:end_sample]
# Лимит GigaAM (25 сек)
if chunk.shape[1] > 25 * sr:
chunk = chunk[:, :int(25 * sr)]
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
tmp_path = tmp.name
try:
# Сохранение чанка через soundfile
sf.write(tmp_path, chunk.cpu().numpy().T, sr)
with torch.no_grad():
result_text = model.transcribe(tmp_path)
if result_text and result_text.strip():
line = f"{format_timestamp(start)} {speaker}: {result_text.strip()}"
final_output.append(line)
print(line)
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
total_time = time.perf_counter() - total_start
print("\n" + "="*50)
print(f"ГОТОВО. Аудио {audio_duration:.1f}с обработано за {total_time:.1f}с.")
print("="*50 + "\n")
return "\n".join(final_output)
if __name__ == "__main__":
try:
final_result = run_pipeline()
save_path = INPUT_AUDIO.rsplit('.', 1)[0] + "_speakers_final.txt"
with open(save_path, "w", encoding="utf-8") as f:
f.write(final_result)
print(f"[Успешно] Результат сохранен в: {save_path}")
except Exception as e:
print(f"\n[Ошибка]: {e}")--- Загрузка GigaAM-v3 (e2e_rnnt) на cpu... ---
--- Декодирование аудио через FFmpeg... ---
--- Запуск Pyannote Diarization... ---
c:\Users\user\V\4 python\gigaam v3\.venv\Lib\site-packages\pyannote\audio\models\blocks\pooling.py:103: UserWarning: std(): degrees of freedom is <= 0. Correction should be strictly less than the reduction factor (input numel divided by output numel). (Triggered internally at C:\actions-runner\_work\pytorch\pytorch\pytorch\aten\src\ATen\native\ReduceOps.cpp:1839.)
std = sequences.std(dim=-1, correction=1)
--- Распознавание 45 сегментов спикеров ---
[00:00:00] SPEAKER_02: Реально, когда заказывал себе вот этот M4 Макс, который сейчас у меня на loptope, я думал Air заказать.
[00:00:09] SPEAKER_02: Поскольку он такой маленький, которую я девочке купил предыдущего поколения, прямо красава. Прекрасный компьютер, летает всё. Девочка предыдущего поколения.
[00:00:18] SPEAKER_02: Девочке купил предыдущую. Не, девочка следующая.
[00:00:19] SPEAKER_00: Нет, девочка следующего поколения всё-таки.
[00:00:22] SPEAKER_02: Девочка, да, слишком следующего поколения. И но останавливало то, что на M4 нет. Как это глупо покупать было на M3. Ну вот всё. Теперь ничего не останавливает вас, дорогие слушатели, можете купить, и я думаю, будет для всех.
[00:00:35] SPEAKER_01: Голубенький. Голубенький. Только голубенький надо брать.
[00:00:38] SPEAKER_02: Так, подожди, а у них же ивента никакого не было, да? Они типа по-тихому как-то это? Да, они даже презентацию
[00:00:41] SPEAKER_00: Да, они даже презентацию не смонтировали, прикинь. Они просто выкатили на сайте.
[00:00:47] SPEAKER_02: А нормально, а нормальные чёрненькие же тоже есть, да?
[00:00:47] SPEAKER_00: Нормальный чёрный.
[00:00:50] SPEAKER_00: Чёрненький, и серебряненький и синенький. Ну, голубенький. Да зачем? Чай блю. Чё, надо голубенький брать?
[00:00:50] SPEAKER_02: Чёрный.
[00:00:58] SPEAKER_01: Смотри, ну конечно, тут, видишь, главная проблема Эйра — ты уверен, что тебе хватит тридцати двух Гов?
[00:01:04] SPEAKER_02: По-моему, 36, нет?
[00:01:06] SPEAKER_01: — Тридцать шесть? — Тридцать да. — Что? Что такое 36? — У них же кривые памяти.
[00:01:09] SPEAKER_02: У них же там кривые цифры в памяти. У них кривые цифры там.
[00:01:13] SPEAKER_01: Не, я не понимаю, что такое 36. Ты понимаешь, что это тупо две планки, две планки по 16 внутри?
[00:01:19] SPEAKER_02: Я не знаю, как они там память отрисуют, но с этими, по-моему, с M3 это началось, там у них память шестнадцать.
[00:01:24] SPEAKER_00: У них 16, 24 и, по-моему, апту 36 всё-таки... Короче.
[00:01:31] SPEAKER_01: Я понял. Сейчас я пойду посмотрю на сайт, я помню, что она, по-моему... Не-не-не, версия 32. Неправильно, 32.
[00:01:37] SPEAKER_02: Тридцать два, говорите. Ну, тридцать два.
[00:01:39] SPEAKER_01: Третье. Короче.
[00:01:40] SPEAKER_02: А смог бы жить в 32? Ну вот реально смог бы жить.
[00:01:45] SPEAKER_00: Такое.
[00:01:46] SPEAKER_02: Ну, жизнь, в жизнью это, конечно, трудно 36
[00:01:46] SPEAKER_00: — В жизни это, конечно, трудно. — А 36 — это, кстати, базовое у новой макстудии на M4. — Да. Самая базовая — это 36.
[00:01:51] SPEAKER_02: Да.
[00:01:53] SPEAKER_02: Тридцать два, 24. Я помню, кривая цифра, базовая 24 у него. А потом 32 идёт.
[00:02:00] SPEAKER_00: У кого-то из предыдущих, кстати, 18 было базовое.
==================================================
ГОТОВО. Аудио 122.7с обработано за 169.8с.
==================================================Привет! Спасибо за статью! А диаризацию (разделение на спикеров) не делаете? Модель это не умеет? Я доберусь попозже сравнить качество русского транскриба с whisper - интересно.
Попиарюсь - сделал недавно на основе whisper, pyannote, runpod своего бота для транскрибации видео и звонков) работает качественно, удобно и быстро, дешево и на сайте регаться не надо, находится в телеге - @slovami_4erez_bot
На спикеров разделяем.
И если исхитриться - можно до двух ГБ поднять размер принимаемого файла в телеге) как у нас;)
От голосовых к тексту: делаем Telegram-бота для расшифровки аудио на модели от СБЕР — GigaAM-v3