Интеграция Whisper.cpp в GNU Radio: SDR-система с распознаванием речи | AiManual
AiManual Logo Ai / Manual.
22 Янв 2026 Гайд

От FM-эфира до текста: как заставить GNU Radio понимать речь через Whisper.cpp

Пошаговое руководство по созданию пользовательского блока GNU Radio для распознавания речи с Whisper.cpp. От компиляции до реального FM-приёмника.

Когда радио начинает понимать слова

Представьте: вы слушаете FM-радио через SDR-приёмник, а на экране параллельно появляется текст того, что говорит диктор. Не магия, а комбинация двух технологий, которые обычно живут в разных вселенных. GNU Radio - для обработки радиосигналов, Whisper.cpp - для локального распознавания речи. Соединить их - задача нетривиальная, но выполнимая.

Ключевой момент: Whisper.cpp на январь 2026 года поддерживает все модели Whisper до v3-large, включая оптимизации для AVX2, AVX512 и NEON. Для embedded-систем есть quantized-версии моделей, которые работают даже на Raspberry Pi 5.

Почему именно эта связка, а не что-то проще?

Можно было бы взять готовый Python-скрипт, который слушает аудиовыход и передаёт его в Whisper. Но это неинтересно. GNU Radio даёт полный контроль над сигнальным трактом: от RF-сигнала до аудио. Можно добавить шумоподавление, эквалайзер, компрессор - всё средствами flowgraph, без переписывания кода.

Ещё один аргумент: производительность. Нативный C++ код Whisper.cpp работает быстрее, чем Python-обёртки. В потоковой обработке каждый миллисекунд на счету.

Что нам понадобится перед началом

  • GNU Radio 3.10 или новее (на январь 2026 актуальна 3.12)
  • Whisper.cpp собранный из исходников
  • Модель Whisper (рекомендую medium или large-v3 - лучший баланс точности и скорости)
  • Базовые знания C++ и Python
  • SDR-приёмник (RTL-SDR, HackRF, LimeSDR - любой)
💡
Если вы ещё не определились с локальным STT-решением, почитайте сравнение WhisperKit, Whisper.cpp и Scriberr. Для чисто офлайн-работы Whisper.cpp - лучший выбор по соотношению скорость/точность.

1 Готовим Whisper.cpp к интеграции

Сначала собираем Whisper.cpp с поддержкой shared library. По умолчанию он собирается как статическая библиотека, нам нужен динамический вариант.

git clone https://github.com/ggerganov/whisper.cpp
cd whisper.cpp
# На январь 2026 актуальна ветка main с поддержкой Whisper v3
make clean
WHISPER_BUILD_SHARED_LIB=on make -j$(nproc)

Проверяем, что библиотека создалась:

ls -la libwhisper.so*  # или libwhisper.dylib на macOS

Теперь скачиваем модель. Для FM-радио с относительно чистым звуком подойдёт medium:

# Скачиваем модель и конвертируем в ggml-формат
./models/download-ggml-model.sh medium

Внимание: large-v3 модель точнее, но требует в 3 раза больше оперативной памяти. Для embedded-систем лучше взять tiny или base. Если нужна максимальная точность для диалектов - смотрите сравнение моделей для детекции диалектов.

2 Создаём скелет OOT-модуля GNU Radio

OOT (Out Of Tree) - способ создать пользовательский блок без модификации исходного кода GNU Radio. Используем gr_modtool:

cd ~
gr_modtool new whisper_sdr
cd whisper_sdr/gr-whisper_sdr
gr_modtool add -t general -l python whisper_transcriber
# Выбираем sync block с float input и string output

Теперь у нас есть заготовка блока. Но это только начало. Нужно понять архитектуру:

Компонент Назначение Сложность
Аудио буфер Накопление 30 секунд аудио (стандарт для Whisper) Средняя
FFT преобразование Передискретизация до 16 кГц Низкая (есть в GNU Radio)
Интерфейс с libwhisper Загрузка модели и инференс Высокая

3 Пишем ядро блока на C++

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

  1. Принимает аудиопоток (float, 48 кГц типично для FM)
  2. Ресемплирует до 16 кГц (Whisper работает только с этой частотой)
  3. Накопливает 30 секунд аудио
  4. Передаёт в Whisper.cpp
  5. Выдаёт текст в Message Queue

Основная проблема: GNU Radio работает в real-time, а распознавание речи - batch-процесс. Решение - двойная буферизация.

// Упрощённая структура блока
class whisper_transcriber_impl : public whisper_transcriber
{
private:
    // Whisper контекст
    struct whisper_context *ctx;
    
    // Аудио буферы
    std::vector audio_buffer;
    std::vector processing_buffer;
    
    // Параметры
    int sample_rate;
    int whisper_sample_rate = 16000;
    
    // Флаги состояния
    bool is_processing;
    
public:
    whisper_transcriber_impl(const std::string &model_path)
      : gr::sync_block("whisper_transcriber",
                      gr::io_signature::make(1, 1, sizeof(float)),
                      gr::io_signature::make(0, 0, 0)),
        is_processing(false)
    {
        // Загружаем модель Whisper
        ctx = whisper_init_from_file(model_path.c_str());
        if (!ctx) {
            throw std::runtime_error("Failed to load Whisper model");
        }
        
        // Создаём порт для вывода текста
        message_port_register_out(pmt::mp("text_out"));
        
        // Буфер на 30 секунд аудио
        audio_buffer.reserve(30 * sample_rate);
    }
    
    ~whisper_transcriber_impl() {
        whisper_free(ctx);
    }
    
    int work(int noutput_items,
             gr_vector_const_void_star &input_items,
             gr_vector_void_star &output_items)
    {
        const float *in = (const float *)input_items[0];
        
        // Добавляем новые семплы в буфер
        audio_buffer.insert(audio_buffer.end(), in, in + noutput_items);
        
        // Если накопили 30 секунд и не обрабатываем сейчас
        if (audio_buffer.size() >= 30 * sample_rate && !is_processing) {
            // Запускаем обработку в отдельном потоке
            std::thread(&whisper_transcriber_impl::process_audio, this).detach();
            
            // Очищаем буфер для новых данных
            audio_buffer.clear();
        }
        
        return noutput_items;
    }
    
    void process_audio() {
        is_processing = true;
        
        // Ресемплируем 48 кГц -> 16 кГц
        std::vector resampled = resample_audio(audio_buffer, sample_rate, whisper_sample_rate);
        
        // Параметры для Whisper
        struct whisper_full_params params = whisper_full_default_params(WHISPER_SAMPLING_GREEDY);
        params.print_realtime = false;
        params.print_progress = false;
        params.print_timestamps = false;
        params.print_special = false;
        params.translate = false;
        params.language = "ru";
        params.n_threads = 4;
        
        // Запускаем распознавание
        if (whisper_full(ctx, params, resampled.data(), resampled.size()) != 0) {
            std::cerr << "Whisper processing failed" << std::endl;
        } else {
            // Получаем результат
            std::string text = get_transcription_text(ctx);
            
            // Отправляем через message port
            pmt::pmt_t msg = pmt::make_dict();
            msg = pmt::dict_add(msg, pmt::mp("text"), pmt::mp(text));
            msg = pmt::dict_add(msg, pmt::mp("timestamp"), pmt::mp(std::time(nullptr)));
            
            message_port_pub(pmt::mp("text_out"), msg);
        }
        
        is_processing = false;
    }
};

Критически важный момент: ресемплинг. Whisper работает ТОЛЬКО с 16 кГц. Если подать другую частоту дискретизации - получите тишину в текстовом виде. Используйте качественный ресемплер (например, libsamplerate), не линейную интерполяцию.

4 Собираем и тестируем блок

Сборка стандартная для OOT-модулей:

mkdir build
cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
make -j$(nproc)
sudo make install
sudo ldconfig

Теперь проверяем, что блок появился в GNU Radio Companion. Но сначала протестируем на файле:

#!/usr/bin/env python3
# test_whisper_block.py

import numpy as np
from gnuradio import gr
from gnuradio.whisper_sdr import whisper_transcriber

# Создаём flowgraph в коде
class TestFlowgraph(gr.top_block):
    def __init__(self):
        gr.top_block.__init__(self)
        
        # Источник - WAV файл
        src = gr.wavfile_source("test_fm.wav", True)
        
        # Наш блок
        whisper = whisper_transcriber("/path/to/ggml-medium.bin")
        
        # Приёмник сообщений
        self.sink = gr.message_debug()
        
        # Соединяем
        self.connect(src, whisper)
        self.msg_connect(whisper, "text_out", self.sink, "store")
        
        # Колбэк при получении сообщения
        def print_text(msg):
            text = pmt.to_python(pmt.dict_ref(msg, pmt.intern("text"), pmt.PMT_NIL))
            print(f"Распознано: {text}")
        
        self.whisper_msg_port = whisper.message_port("text_out")
        self.whisper_msg_port.set_msg_handler(print_text)

if __name__ == "__main__":
    tb = TestFlowgraph()
    tb.start()
    tb.wait()

5 Собираем полную SDR-систему

Теперь самое интересное - целый FM-приёмник с распознаванием речи. В GNU Radio Companion создаём flowgraph:

  1. RTL-SDR Source (частота вашей FM-станции)
  2. WBFM Receive (стандартный блок для FM-стерео)
  3. Rational Resampler (48 кГц → 16 кГц)
  4. Наш whisper_transcriber блок
  5. QT GUI Text Sink или File Sink для вывода текста

Параметр, который все забывают настроить: squelch (шумоподавитель). Без него Whisper будет пытаться распознавать шум между передачами. Добавьте блок "Simple Squelch" после FM-демодулятора.

💡
Для промышленного использования такой системы посмотрите статью про транскрибацию совещаний. Там много полезного про оптимизацию и развёртывание.

Оптимизации, без которых система будет тормозить

Наивная реализация работает, но съедает CPU. Вот что нужно улучшить:

1. Кэширование модели в памяти

Whisper.cpp загружает модель при каждом создании блока. В GNU Radio блоки могут пересоздаваться при изменении flowgraph. Решение - singleton-кэш:

// Глобальный кэш моделей
static std::map model_cache;

whisper_context* get_cached_model(const std::string& path) {
    std::lock_guard lock(cache_mutex);
    if (model_cache.find(path) == model_cache.end()) {
        model_cache[path] = whisper_init_from_file(path.c_str());
    }
    return model_cache[path];
}

2. Асинхронная обработка с thread pool

Создание потока для каждого 30-секундного фрагмента - overhead. Используйте готовый пул потоков:

#include 

static ThreadPool pool(2);  // 2 потока для распознавания

// Вместо std::thread(...).detach()
pool.enqueue([this, audio_copy]() {
    this->process_audio(audio_copy);
});

3. Quantized модели для embedded

Если система работает на Raspberry Pi или аналогичном железе, используйте квантованные модели:

# Вместо medium скачиваем quantized версию
./models/download-ggml-model.sh medium-q5_0
# Или даже tiny для реального времени
./models/download-ggml-model.sh tiny-q8_0

Типичные ошибки и как их избежать

Ошибка Причина Решение
Whisper возвращает пустую строку Неправильная частота дискретизации или тихий сигнал Добавить блок "AGC" перед ресемплером, проверить 16 кГц
Задержка больше 30 секунд Обработка занимает больше времени, чем запись Использовать smaller модель или увеличить thread pool
Сегфолт при остановке flowgraph Whisper работает в фоновом потоке при разрушении объекта Добавить флаг остановки и join потоков в деструкторе
Плохое качество распознавания FM-сигнал с шумами, multipath-искажения Добавить блоки "Low Pass Filter" и "Noise Reduction"

Что можно сделать дальше?

Система работает, но это только начало. Дальнейшие улучшения:

  • VAD (Voice Activity Detection) - не ждать 30 секунд, а определять начало и конец речи. Есть готовые блоки для GNU Radio
  • Диаризация - определять, кто говорит. Для этого понадобится что-то вроде Speakr с диаризацией
  • Streaming-режим Whisper - Whisper.cpp поддерживает streaming API с 2024 года. Можно получать текст по мере обработки, а не ждать 30 секунд
  • Многоканальность - обрабатывать несколько FM-станций параллельно
  • Интеграция с VoIP - транскрибировать не радио, а SIP-звонки

Интересный факт: на DGX Spark с GPU-ускорением можно достичь задержки менее 766 мс от приёма сигнала до текста. Подробности в статье про голосового ассистента на DGX Spark.

Почему это вообще работает в реальном времени?

Секрет в асинхронности. Пока Whisper обрабатывает предыдущие 30 секунд, система продолжает записывать следующие. На современном процессоре (Intel i5/i7, AMD Ryzen) medium-модель обрабатывает 30 секунд аудио за 2-3 секунды. Значит, мы всегда отстаём всего на 2-3 секунды от реального времени.

Для FM-радио это приемлемо. Для двусторонней радиосвязи (рации) - уже нет. Там нужны tiny-модели и оптимизации.

Самое сложное в этой системе - не код, а настройка FM-приёмника. Плохой сигнал = плохая транскрипция. Whisper терпеть не может шум, клиппинг и искажения. Потратьте 80% времени на настройку RF-цепи, 20% - на код.

И последнее: эта система - proof of concept. Для продакшена нужна обработка ошибок, мониторинг, логирование и тесты. Но как отправная точка для экспериментов со SDR и AI - идеально.

Кстати, тот же подход работает для DAB+, DRM и даже аналогового TV с аудиодорожкой. Меняется только демодулятор в GNU Radio. Whisper-блок остаётся тем же. Вот что значит modular design.