Дисклеймер: Автор признаёт, что приведённый ниже код может быть далёк от идеала. Его цель — показать, как легко создать подобную утилиту своими руками. Также автор не исключает, что при решении отдельных задач с кодом обращался за помощью к нейросетям.

Я — .NET-разработчик, и уже некоторое время прохожу собеседования. Иногда на них задают вопросы про такие «пыльные уголки» .NET, что если я и читал про них, то лишь в самом начале своего пути, листая книги Рихтера, Троелсена и Шилдта.
А ещё встречаются откровенно странные задачи, далёкие от реальной работы.
Зачем всё это? Очевидно, чтобы отсечь новичков из онлайн-курсов и тех, кто «вкатился» в IT без должной подготовки.
Ну а раз на собеседовании всё изначально настроено так сказать «враждебно», и интервьюер подозревает, что каждый мухлюет… почему бы действительно не начать мухлевать?
Сначала я полез в интернет в поисках готового решения и наткнулся на WiseWhisper.
Эта утилита создаёт полупрозрачный оверлей, слушает звуки с ПК и подсказывает ответы в реальном времени. Звучит идеально… но стоит $29 в месяц. Довольно дорого, согласитесь, а бесплатная версия предлагает всего 10 подсказок.
Тогда я подумал: а почему бы не написать своё решение?
Так началось моё R&D.
R&D
Поковыряв WiseWhisper, я понял, что программа не делает ничего сверхъестественного:
Слушает звуки с ПК.
Переводит их в текст.
Отправляет текст в нейросеть и отображает ответ в оверлее.
Задачи вроде бы простые, но прежде чем их решать, я хотел убедиться, насколько легко скрыть окно программы от лишних глаз.
Спросил первым делом у ChatGPT — и он предоставил простой, рабочий вариант, который я тут же протестировал на WPF-приложении:
private const uint WDA_EXCLUDEFROMCAPTURE = 0x11;[DllImport("user32.dll")]private static extern bool SetWindowDisplayAffinity(IntPtr hWnd, uint dwAffinity);protected override void OnSourceInitialized(EventArgs e){ base.OnSourceInitialized(e); var hwnd = new WindowInteropHelper(this).Handle; SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE);}

Это, так сказать, вдохновило меня, и я принялся искать решения для следующих задач.
Работа со звуком

Для работы со звуком я выбрал NAudio.
Логика была довольно простой:
Создаём
WasapiLoopbackCapture.Подписываемся на событие
DataAvailable, в котором записываем данные черезWaveFileWriterвMemoryStream.Если звука не было в течение N времени, создаём файл
.wavи передаём его дальше.
Примерно так выглядела вся инициализация:
_capture = new WasapiLoopbackCapture();
audioBuffer = new MemoryStream();audioBufferWriter = new WaveFileWriter(_audioBuffer, WaveFormat.CreateIeeeFloatWaveFormat(_capture.WaveFormat.SampleRate, capture.WaveFormat.Channels));capture.StartRecording();
Проблема была в том, что винда отсылает "звук", даже когда его нету и поэтому мне пришлось написать вот такой метод для фильтрации:
private bool IsSilence(WaveInEventArgs e, float threshold = 0.0005f)
{
int samplesCount = e.BytesRecorded / 4;
var samples = new float[samplesCount];
Buffer.BlockCopy(e.Buffer, 0, samples, 0, e.BytesRecorded);
foreach (var sample in samples)
{
if (Math.Abs(sample) > threshold)
return false;
}
return true;
}
Речь в текст

Следующим этапом было преобразование речи в текст.
На моём ПК уже был установлен Whisper, который умеет это делать. Первым делом я попытался интегрировать его в программу через командную строку, но это оказалось не самым оптимальным вариантом: Whisper каждый раз при запросе подгружал модель, и процесс был медленным.
Решением стала библиотека Whisper.net, которая позволяет удобно и быстро взаимодействовать с моделью.
Для работы необходимо скачать модель и указать путь к ней при создании через WhisperFactory. Я использовал модель ggml-small.bin.
Примерно так выглядел весь код:
whisperProcessor = WhisperFactory.FromPath(modelPatch).CreateBuilder().WithLanguage("auto").Build();
var stringBuilder = new StringBuilder();
await using var fileStream = File.OpenRead(filePath);
await foreach (var segment in whisperProcessor.ProcessAsync(fileStream))
{
stringBuilder.Append(segment.Text);
}
return stringBuilder.ToString();
Правда, на этом этапе я столкнулся с проблемой: Whisper не поддерживает 32-битный звук. Поэтому на этапе записи аудио пришлось добавить конвертацию из 32 бит в 16 бит, примерно так:
private byte[] To16KSample()
{
using var memoryStream = new MemoryStream(_audioBuffer.ToArray());
using var reader = new WaveFileReader(memoryStream);
using var resampler = new MediaFoundationResampler(reader, new WaveFormat(16000, 16, 1))
{
ResamplerQuality = 60
};
using var ms16k = new MemoryStream();
WaveFileWriter.WriteWavFileToStream(ms16k, resampler);
return ms16k.ToArray();
}
Отправка текста в нейросеть

И, наконец, заключительный этап — отправить текст в нейросеть и получить ответ.
Для этого я воспользовался Ollama и её удобной библиотекой OllamaSharp:
public void Start(){ var http = new HttpClient { BaseAddress = new Uri("http://localhost:11434") }; http.DefaultRequestHeaders.Add("x-api-key", "KEY"); client = new OllamaApiClient(http,"deepseek-v3.1:671b-cloud"); chat = new Chat(_client); Task.Run(async () => { await foreach (var in chat.SendAsync("Помоги мне с собеседованием. Отвечай коротко на вопросы")) { } }).GetAwaiter().GetResult();}public IAsyncEnumerable<string?> AskAsync(string question){ return _chat.SendAsync(question);}
Ключ можно легко и бесплатно получить на их официально сайте.
В итоге все выглядит как-то так:

Весь код проекта я пока не прикладываю — прежде хочу его немного «причесать» и сделать более аккуратным.
