
В фан-сообществах Модели для сборки регулярно всплывают похожие сообщения:
«Помогите вспомнить рассказ… Там был мальчик… робот… и какой-то поворот в конце…»
Десятки комментариев, версии, догадки - почти коллективная реконструкция сюжета. Я каждый раз ловил себя на мысли, что и сам бывал в этой ситуации. Ты помнишь эмоцию, отдельный эпизод, звук голоса диктора - а название потерялось. Сегодня у слушателей есть один рабочий инструмент: спросить других.
В какой-то момент я понял, что больше не хочу наблюдать это со стороны. Так появилась идея создать в приложении МДС Коллекция поиск по описанию - не по ключевым словам, а по смыслу. Чтобы человек мог написать несколько фраз, как сумел сформулировать, и получить те самые рассказы.
Но идея упёрлась в фундаментальную проблему.
У меня не было текстов.
У меня были только аудиозаписи. С фоновой музыкой и проблемами с громкостью.
Whisper, ffmpeg и 1600 рассказов, которые нужно превратить в текст
МДС Коллекция - это примерно 1600 рассказов, более месяца непрерывного прослушивания. Записаны они в разное время, разными людьми, на разном оборудовании. Whisper, как и большинство моделей ASR, любит чистые, стабильные WAV-файлы. У меня были MP3, некоторые - повреждённые.
Пайплайн казался очевидным:
ffmpeg → Whisper → текст
Но масштаб означал, что работать «в лоб» нельзя - это заняло бы недели.
Варианты ускорения были простыми:
- Одолжить у друга GPU (RTX 3060 Ti) и распараллелить скрипт.
- Арендовать GPU-сервер у Hetzner.
- Запустить всё локально на Apple M4 через Metal.
Экономика и удобство победили - я отнёс задачу другу.
И это сработало: три дня, три потока whisper.cpp, модель ggml-large-v3 - и все 1600 аудиофайлов превратились в текст.
Я выборочно проверил качество - оно казалось приемлемым. Но только до тех пор, пока я не начал строить поиск.
Когда поисковая модель говорит: «Я не знаю этот рассказ», хотя он должен быть найден
На этапе создания векторного поиска я заметил странность: некоторые рассказы не находились даже по точному описанию, хотя содержали соответствующие фразы.
Открываю текст - вижу искажения, пропуски, неправильно распознанные слова. Whisper сделал своё дело, но далеко не идеально.
Переписывать всю коллекцию заново я не хотел. Нужно было:
- найти «кривые» тексты,
- обработать только их,
- повысить качество транскрибации.
Первую задачу решил простой shell-скрипт: эвристики, подсчёт плотности текста, анализ резких провалов по длине фрагментов.
Так нашлось около 200 проблемных файлов.
Теперь требовалось улучшить качество модели.
Как звук влияет на смысл: ffmpeg, VAD и вторая волна транскрибаций
При углублённом изучении Whisper стало ясно, что качество можно существенно улучшить двумя вещами:
1. Предобработка аудио
ffmpeg отлично справляется с задачами:
- усиление речи относительно фона,
- подавление шумов,
- нормализация громкости.
Whisper после такой очистки слышит уже не «аудиофайл с музыкой», а голос, который он и обучен распознавать.
2. Voice Activity Detection (VAD)
VAD размечает участки, где есть человеческая речь. Whisper может пропускать тишину, искажения и бессмысленные фрагменты. Это:
- ускоряет процесс,
- уменьшает контекстные ошибки,
- повышает точность.
- Эти два шага в сумме дают гораздо более чистую транскрипцию.
GGML, GGUF и почему не CoreML
Техническое отступление.
GGML - лёгкий формат, оптимизированный под CPU.
GGUF - его преемник: более структурированный, с квантованием, удобными метаданными и лучшей совместимостью. Сегодня практически все Whisper-модели распространяются в GGUF.
Почему не CoreML?
Потому что под CoreML доступна только версия Whisper v2.
Она хуже по качеству, чем v3-модели, особенно на шумных и старых записях.
Поэтому вторую волну транскрибаций я делал локально, на Apple M4 с Metal backend, используя ggml-модель.
Результат: качество выросло настолько, что поисковая система наконец начала работать так, как задумано.
От текста - к поиску: как построить семантическую систему для 30-летнего архива
Теперь у меня был корпус текстов, отражающий не только сюжеты, но и звучание эпохи.
Это важный момент:
МДС появилась более 30 лет назад, и ранние выпуски несут в себе культурный слой конца 90-х. Рекламные вставки клубов, объявления «для взрослых», шумы эфира - это тоже часть памяти слушателей.
Я хотел сперва их удалить, но после разговора с поклонниками передачи понял: люди ищут не только по сюжету, но и по этим случайным маркерам времени.
Очищать их - значит терять часть ключей для поиска.
Как устроен поиск: от человеческого описания до пяти самых подходящих рассказов
Структура моего поискового пайплайна выглядит так:
- Пользователь вводит описание - иногда размытое, иногда в одно предложение.
- Сервер преобразует описание в вектор - числовое представление смысла.
- Этот вектор идёт в векторную базу данных, где ищутся 30 ближайших фрагментов текста.
- Срабатывает реранкинг - уже не фрагментов, а историй целиком.
- Пользователь получает топ-5 наиболее вероятных совпадений.
Снаружи всё выглядит просто, но внутри крутится несколько ключевых решений.
Почему именно Qdrant
Сегодня многие базы заявляют, что умеют векторный поиск: PostgreSQL, MongoDB, ClickHouse. Они действительно умеют, но как надстройку.
Мне нужен был инструмент, который:
- создавался для векторного поиска,
- быстрый,
- лёгкий,
- надёжен,
- разворачивается в один docker run,
- open source.
Таким инструментом оказался Qdrant.
Rust под капотом, минималистичная архитектура, удобный API - ровно то, что нужно мобильному сервису с быстрыми ответами.
Выбор эмбеддинговой модели: BGE-M3
Модель должна уметь понимать человеческий способ описания. Люди не пишут так: «пожалуйста, найди мне рассказ, содержащий признаки X, Y, Z». Они пишут:
«там был поезд… кажется, девушка… что-то таинственное…»
Модель BGE-M3 оказалась идеальным вариантом:
- отлично работает на длинных текстах (целых рассказах),
- понимает неформальные описания,
- находит смысловые пересечения даже в туманных запросах,
- не требует мощной видеокарты.
Она стала основой качества поиска.
Как работает реранкинг
Векторная база отдаёт не сами рассказы, а фрагменты. Один рассказ может дать один релевантный фрагмент, другой - пять.
Если брать только лучший фрагмент, можно потерять истории, где совпадений больше, но каждое чуть слабее.
Поэтому я ввёл формулу, которая:
- усиливает вклад лучшего совпадения,
- добавляет вес остальных релевантных фрагментов,
- даёт итоговый story score для каждой истории.
Логика проста:
история, у которой несколько совпадающих фрагментов, скорее всего ближе к запросу, чем история с единственным случайным попаданием.
Первые результаты - и первое разочарование
Когда я собрал всё в FastAPI, завернул в Docker и развернул сервис, результаты были вдохновляющими.
Для подробных запросов поиск работал как надо: точный, быстрый, уверенный.
Но вскоре проявились два ограничения:
1. Большинство людей пишут очень коротко
Иногда всего одно слово.
Модель умеет работать с малым контекстом, но качество таких запросов естественно хуже.
2. Время запроса оказалось выше желаемого
На холодной системе: 600–1000 мс.
На p99 под нагрузкой - до 3 секунд.
Для мобильного приложения это многовато.
Система работала, но не была оптимизирована.
И стало ясно: впереди ещё одна важная глава - ускорение пайплайна, улучшение модели работы с короткими описаниями и дальнейшее развитие поиска.


Наивный вопрос - а почему по автору и названию не найти оригинальные тексты в условной Флибусте?