Мультиплеер для интровертов: пишем голосового компаньона для Euro Truck Simulator 2
В Euro Truck Simulator 2 очень тихо. Мотор, поворотник, навигатор. Можно включить радио, можно позвать друзей в конвой. А можно сделать так, чтобы на пятнадцатом канале кто‑то был.
«Канал 15» — бесплатная программа‑компаньон для ETS2: имитация радиоканала дальнобойщиков. В эфире восемнадцать выдуманных водителей с голосами, характерами, сменами, маршрутами и грузами. Игрок жмёт кнопку на руле и говорит вслух. Ему отвечают.
Это не мод: игру программа не модифицирует, ставит только стандартный плагин телеметрии в папку плагинов и читает данные из общей памяти игры. Проекту около двух недель активной работы, живая версия — 1.34. Ниже — как это устроено и где я наступил на грабли.
Конвейер: тангента → текст → намерение → реплика
кнопка руля ─► микрофон (буфер 0,4 с до нажатия)
─► faster-whisper small, CPU, int8
─► классификатор намерений
─► колода реплик ─► радиоэффект по дистанции ─► динамики
Всё локально. Голос игрока никуда не уходит — это было условием с первого дня.
Распознавание. faster‑whisper small на CPU, int8, четыре потока. Микрофон открыт постоянно, но пишет только пока зажата тангента, плюс 0,4 секунды до нажатия — люди начинают говорить чуть раньше, чем нажимают. Аудио идёт блоками по 50 мс: двадцать колбэков в секунду вместо ста, 0,8% ядра вместо 2,2%. Мелочь, но рядом работает игра, которой нужно всё.
Модель весит около 2,2 ГБ памяти. Держать её всё время рядом с ETS2 жалко, поэтому она грузится по нажатию тангенты (около 1,7 секунды — успевает, пока игрок произносит фразу) и выгружается через четыре минуты тишины.
Подсказка для whisper. Без неё «кто в эфире?» стабильно превращалось в «кто вы, Хире?» или «кто вы, Сири?». Помогает initial_prompt со словарём канала: «Рация, канал дальнобойщиков. Кто в эфире? Повтори. Приём.» — и позывные всех водителей. Одна строка — и половина ошибок распознавания ушла.
Вторая половина — галлюцинации. На коротких и тихих фрагментах whisper уверенно выдаёт «Продолжение следует…», «Спасибо за просмотр» и «Редактор субтитров». Он учился на субтитрах YouTube и честно дописывает то, что там обычно в конце. Лечится стоп‑листом.
Мёртвый микрофон или тихий человек. Частая жалоба: «рация меня не слышит». Причины две: выбран не тот микрофон (виртуальный, «Стерео микшер») или человек говорит тихо. Отличить можно по разбросу пикового и среднего уровня. Живой микрофон дышит даже в тихой комнате — разброс десятки децибел. Мёртвое устройство — плоская линия, разброс пара децибел. Первому рация говорит «говорите ближе», второму — «выберите другой микрофон».
Намерения
Дальше — классификатор намерений: приветствие, «кто в эфире?», вопрос про конкретного человека, зов того, кого нет рядом, просьба, молчаливое слушание. Это не нейросеть, а основы слов и правила. Главная работа здесь — не точность, а то, что делать, когда не понял.
Колода вместо генерации
Все реплики озвучены заранее, на лету ничего не синтезируется. Отсюда ограничение: в общей версии игрока не зовут по имени — его имени в звуке просто нет.
Эфир собирается из «колоды» с защитой от повторов: у реплики есть вес, время последнего проигрыша и счётчик. Байки идут цепочками: водитель может начать историю сегодня и закончить через пару рейсов. Сейчас в общей сборке больше трёх тысяч звуковых файлов.
Слышно только тех, кто рядом. У каждого водителя своя смена и маршрут по 382 городам. Чем дальше водитель, тем хуже сигнал: радиоэффект накладывается по дистанции, и на краю зоны голос тонет в шипении.
Граф и дружба
Между водителями есть граф знакомств: кто кого знает, кто с кем в ссоре. С игроком — шкала дружбы: от сдержанного «ну, здорово» до приглашения на свадьбу к брату. Порог «знакомого» — 35 очков.
Плохие дни и ссоры сделаны специально. Если все всегда добрые, выходит сироп, а тепло чувствуется только рядом с плохими днями.
Пришлось закрыть и накрутку: дружба росла от перезапусков программы и от пустых ответов. Теперь пустой ответ даёт ноль очков.
Грабли
0xc0000374. Рация падала молча: ни в логе, ни в журнале ошибок — процесс просто исчезал. Код 0xc0000374 — повреждение кучи, падение внутри winmm.dll, стек ведёт в никуда, воспроизводится не у всех. Закономерность нашлась по датам в дампах: падение совпадало с отключением USB‑руля во время опроса кнопок через joyGetPosEx. Исключение рождается в нативном коде, Python‑шный try/except его не видит. Лечение — опрос кнопок через SDL (pygame): там отвал джойстика приходит событием, а не крашем.
«Нет данных». Первая версия на «кто в эфире?» отвечала роботом: «Нет данных». Честно — в зоне никого. Но пользователи читали это как поломку. Робота уволили: теперь в пустом эфире отвечает водитель с дальнего края зоны, в своём характере.
«Стасиан». Whisper слышит «Стасиан» вместо «Стас». А когда игрок звал водителя, который уехал из зоны, эфир отвечал общим «не знаю» вместо человеческого «далеко уехал, не достучишься». Теперь у каждого водителя своя уклончивая реплика на такой случай.
Ремарки вслух. В тексты реплик вставлялись ремарки вроде «смеётся». Часть из них голос прочитал словами. Нашли так: прогнали весь эфир, 639 строк, обратно через whisper и поискали ремарки в распознанном тексте. Нашлось 46 строк. Переозвучили.
Телеметрия «blocked, код 2». Иногда Windows не пускает сторонний процесс к memory‑mapped файлу игры. Для игрока это выглядит как «рация молчит». Лечится перезапуском игры. Почему именно так — вопрос в комментарии, буду рад версиям.
Зачем
Мне хотелось место, где тебя помнят и тебе рады, но ничего от тебя не требуют. Никаких счётчиков «дней подряд»: вернулся через месяц — тебе рады. Вокруг люди, разговор идёт, а отвечать не обязательно. Получилось что‑то вроде мультиплеера для интровертов.
Вопросы к инженерам, ради которых я это и пишу: как бы вы отличали обращение к водителю от разговора игрока с кем‑то в комнате? И что вы делаете с галлюцинациями whisper на коротких фрагментах, кроме стоп‑листа?
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.