Pull to refresh

Comments 12

Чанки по 25 секунд, а как потом склеивать разорванные на полуслове предложения?

Зачем тут телеграм? Если надо читать голосовухи которые там ходят то вроде сберовский бот с ними справляется @smartspeech_sber_bot А длинные надо как то обрабатывать (сразу закидывать в какую то нейросеть которая всё умеет, типа джемини), что толку с огромной записи голоса, не будешь же ты это реально всё читать.

Я тоже пробовал телеграмм бота создать, получилось. Но решил его в программу сконвертировать для windows , чтобы файлы оставались локально, для безопасности. В итоге получилась практичная программулина, в которую можно закидывать хоть по гигабайт аудиофайлов запятая и оно всё расшифрует и сделает протокол совещания. Так и назвал: Программа протокола совещания.

Вы это сообщение с помощью своей программы транслировали? А то у вас "запятая" текстом написана)

Нет, я просто за рулём еду, поэтому проще надиктовать

Прикольно. Нейронка локальная или расшифровка через облако? В опенсорсе есть она? Интересно было бы посмотреть ее.

Можете в Яндекс забить: программа протокола совещания softneo. Там демо версия есть, можно три запуска делать..

Подскажите, сколько видео памяти надо чтобы запустить модель? Ну или железо на котором запускали

На данный момент мой бот запущен на сервере с 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с.
==================================================

Увидел у вас pyannote. Это требовани модели Сбера? Или чтобы диаризацию сделать? Она же платная по лицензии для коммерческого использования

Это одноразовый скрипт написанный нейронкой. Просто хотел посмотреть как быстро оно работает на цпу. Лицензия неважна.

Тут оно нужно только для диаризации.

Привет! Спасибо за статью! А диаризацию (разделение на спикеров) не делаете? Модель это не умеет? Я доберусь попозже сравнить качество русского транскриба с whisper - интересно.

Попиарюсь - сделал недавно на основе whisper, pyannote, runpod своего бота для транскрибации видео и звонков) работает качественно, удобно и быстро, дешево и на сайте регаться не надо, находится в телеге - @slovami_4erez_bot

На спикеров разделяем.

И если исхитриться - можно до двух ГБ поднять размер принимаемого файла в телеге) как у нас;)

Sign up to leave a comment.

Articles