22 KiB
Max Index — контракт для агента
Что мы создаём
Max Index — небольшое кроссплатформенное desktop-приложение для дейликов и других разговоров.
Приложение:
- Слушает выбранный источник звука.
- Локально распознаёт русскую речь.
- Находит заранее настроенные фразы вроде «это сделать легко».
- Увеличивает индекс от 0 до 100.
- Немедленно меняет лицо персонажа и показывает короткую реакцию.
Продукт должен ощущаться как смешной живой индикатор встречи, а не как тяжёлая система транскрибации.
Какой результат хотим получить
Первая полноценная версия должна:
- работать на Windows, macOS и Linux;
- запускаться как обычное desktop-приложение и уметь жить в tray;
- по умолчанию слушать микрофон;
- показывать, что прослушивание включено или остановлено;
- позволять выбрать аудиоустройство;
- распознавать речь локально, без облачного API;
- поддерживать редактируемые правила: варианты фразы, величина увеличения и cooldown;
- увеличивать индекс строго один раз на одно произнесение;
- ограничивать индекс диапазоном
0..100; - показывать последнюю сработавшую фразу и величину изменения;
- позволять поставить прослушивание на паузу и сбросить индекс;
- не сохранять сырой звук;
- не сохранять полный транскрипт по умолчанию;
- не блокировать интерфейс во время распознавания;
- понятно объяснять ошибки разрешений, модели и аудиоустройства.
Границы первой версии
В первую версию входят:
- режим микрофона;
- локальное потоковое распознавание;
- список правил фраз;
- индекс и история срабатываний текущей сессии;
- несколько состояний лица;
- Start, Pause и Reset;
- выбор микрофона;
- сборка установщиков для основных desktop-платформ.
В первую версию не входят без отдельного запроса:
- аккаунты и серверная часть;
- синхронизация между устройствами;
- мобильные приложения;
- запись встреч;
- полная стенограмма;
- определение, кто именно произнёс фразу;
- обучение собственной speech-to-text модели;
- сложная аналитика и рейтинги сотрудников;
- обязательный захват системного звука.
Захват звука компьютера добавить отдельным вертикальным срезом после стабильного режима микрофона. Он нужен для встреч в наушниках, но имеет разные ограничения на Windows, macOS и Linux.
Основной стек
- Electron.
- TypeScript в strict-режиме.
- React для интерфейса.
- Vite для разработки и сборки renderer-кода.
- Electron Forge для установщиков.
- Web Audio API и
AudioWorkletдля потокового PCM. utilityProcessдля локального распознавания.sherpa-onnxс русской streaming-моделью T-one как первый recognizer.- Адаптер recognizer, позволяющий позже проверить
whisper.cppбез переделки приложения. - Vitest для unit-тестов.
- Playwright или эквивалентный Electron smoke-test только там, где он реально окупается.
Если в репозитории уже принят другой package manager, formatter или test runner, следовать репозиторию. В новом репозитории использовать pnpm.
Архитектура
Разделять приложение на процессы и чистую предметную логику:
Renderer
├─ React UI
├─ выбор источника
└─ Web Audio / AudioWorklet
│ PCM-блоки
▼
Utility process
├─ ресемплинг и буферизация
├─ speech-to-text adapter
└─ partial/final transcript events
│ текст
▼
Domain
├─ нормализация текста
├─ поиск правил
├─ защита от дублей
└─ индекс 0..100
│ события
▼
Renderer
├─ лицо
├─ индекс
└─ история срабатываний
Ответственность процессов
main:
- жизненный цикл Electron;
- окна и tray;
- безопасные IPC-каналы;
- разрешения операционной системы;
- путь к модели и настройки;
- запуск и восстановление utility-процесса.
preload:
- только узкий типизированный API через
contextBridge; - никаких произвольных вызовов Node.js из renderer;
- никаких универсальных
send(channel, payload)наружу.
renderer:
- интерфейс;
- получение MediaStream;
- AudioWorklet;
- отображение состояния;
- отсутствие тяжёлого inference-кода.
utility process:
- загрузка нативного recognizer;
- обработка PCM;
- потоковое распознавание;
- контроль очереди и backpressure;
- возврат текста и диагностических состояний.
domain:
- чистые TypeScript-функции без Electron и React;
- правила фраз;
- deduplication и cooldown;
- вычисление индекса;
- выбор визуального состояния.
Предлагаемая структура
src/
main/
preload/
renderer/
components/
features/
audio/
capture/
recognition/
domain/
triggers/
index/
face/
shared/
resources/
models/
faces/
tests/
fixtures/
Не создавать все каталоги заранее. Добавлять структуру по мере появления реального кода.
Контракт аудио
- Получать микрофон через
navigator.mediaDevices.getUserMedia. - Получать сырой звук через
AudioWorklet, а не строить основной realtime-путь наMediaRecorder. - Передавать mono PCM блоками примерно по 100–300 мс.
- Не полагаться на то, что ОС или Chromium действительно выдали запрошенный sample rate.
- Выполнять ресемплинг в одном явно определённом месте.
- Не отправлять один IPC-вызов на каждый 128-sample worklet frame.
- Не использовать синхронный IPC.
- Останавливать MediaStream tracks при Pause, смене источника и завершении приложения.
- Использовать идентификатор audio session и игнорировать запоздалые результаты старой сессии.
- Ограничивать очередь PCM; при перегрузке сообщать состояние, а не бесконечно накапливать память.
- Не писать сырой звук на диск без отдельного явного debug-режима.
Контракт распознавания
Определить небольшой интерфейс recognizer:
interface SpeechRecognizer {
start(config: RecognitionConfig): Promise<void>;
acceptPcm(chunk: Float32Array): void;
stop(): Promise<void>;
onEvent(handler: (event: RecognitionEvent) => void): () => void;
}
Не протаскивать API конкретной библиотеки в UI или domain-логику.
События должны различать:
- partial transcript;
- final transcript;
- ready/listening state;
- recoverable error;
- fatal model error.
Каждое transcript-событие должно содержать:
sessionId— запуск аудиосессии;utteranceId— одно отдельное произнесение;sequence— номер ревизии текста внутри произнесения;kind—partialилиfinal;text.
Все partial и final одного произнесения обязаны иметь одинаковый utteranceId. Если recognizer не предоставляет такой идентификатор, adapter должен синтезировать его на основе endpointing, не перекладывая эту задачу на UI.
Распознавание должно работать локально. Любой облачный вариант добавлять только как явную необязательную функцию после согласования.
Контракт правил и индекса
Минимальная модель правила:
interface TriggerRule {
id: string;
title: string;
patterns: string[];
delta: number;
cooldownMs: number;
enabled: boolean;
}
Перед сопоставлением:
- привести текст к нижнему регистру;
- заменить
ёнае; - нормализовать пробелы;
- убрать незначащую пунктуацию;
- учитывать границы слов.
Сначала использовать точные варианты фраз. Нечёткое сравнение по умолчанию выключить: оно легко создаёт ложные срабатывания.
Streaming ASR может вернуть одну фразу в нескольких partial-результатах. Одно произнесение обязано создавать только одно TriggerMatched событие. Идентифицировать совпадение по sessionId + utteranceId + ruleId + позиции фразы. Cooldown проверять после дедупликации: это дополнительная защита, но не замена корректной идентичности события.
Если occurrence был подавлен cooldown, всё равно пометить его обработанным, чтобы поздний final того же utterance не сработал повторно.
Индекс изменять только через чистую функцию:
next = Math.max(0, Math.min(100, current + delta));
React-компоненты не должны самостоятельно искать фразы или изменять индекс.
Визуальный контракт
Главный элемент окна — лицо персонажа и индекс. Остальные элементы не должны с ними конкурировать.
Базовые уровни:
| Индекс | Состояние |
|---|---|
| 0–19 | спокойный |
| 20–39 | настороженный |
| 40–59 | раздражённый |
| 60–79 | на грани |
| 80–99 | критический |
| 100 | MAXIMUM |
На срабатывание показывать короткую transient-реакцию, затем возвращаться к состоянию текущего уровня.
Использовать оригинального персонажа и оригинальные ассеты. Не копировать спрайты Doom. Допустимо повторить принцип HUD-лица: несколько базовых эмоций, idle-взгляды и отдельный кадр реакции.
Предпочитать PNG/WebP sprite sheet и простую state machine. Не добавлять тяжёлый animation framework без необходимости. Уважать системную настройку reduced motion.
Интерфейс первой версии
Основное окно должно содержать:
- лицо;
- крупное значение индекса;
- состояние
Listening,Paused,No permission,Model error; - Start/Pause;
- Reset;
- последнюю найденную фразу;
- компактный переход к настройкам.
Настройки должны содержать:
- аудиоустройство;
- список trigger rules;
- изменение
deltaи cooldown; - тест микрофона;
- диагностический transcript preview, явно помеченный как временный.
Не показывать технические термины вроде PCM, ONNX или endpointing обычному пользователю. Ошибки писать человеческим языком и давать одно понятное действие.
Приватность
- Всегда явно показывать активное прослушивание.
- Не запускать микрофон скрытно.
- Не сохранять звук.
- Не вести полную историю распознанной речи по умолчанию.
- В обычных логах не писать полный transcript.
- Хранить только настройки и короткие события сработавших правил.
- Предусмотреть понятную очистку истории текущей сессии.
- Перед использованием на рабочей встрече напомнить пользователю учитывать правила компании и согласие участников.
Кроссплатформенные требования
- Не собирать пути строковой конкатенацией; использовать
pathи Electron resource paths. - Не считать, что бинарник, модель или библиотека одинаковы для всех OS/arch.
- Проверять Windows x64, macOS arm64/x64 и Linux x64 как отдельные release targets.
- На macOS предусмотреть описания разрешений микрофона и системного аудио в
Info.plist. - На Windows корректно объяснять отключённый глобальный доступ desktop-приложений к микрофону.
- На Linux не обещать одинаковую работу loopback без проверки PipeWire/PulseAudio окружения.
- После первого получения модели приложение должно уметь работать offline.
- Проверять целостность загружаемой модели и не начинать распознавание с неполным файлом.
Правила написания кода
- Писать названия типов, файлов и переменных на английском; пользовательский интерфейс первой версии — на русском.
- Использовать строгие типы; не вводить
anyради быстрого исправления. - Держать domain-логику независимой от Electron, React и конкретного recognizer.
- Не создавать абстракции до появления второго реального варианта, кроме явно нужного
SpeechRecognizerи audio source boundary. - Не смешивать сбор аудио, распознавание, matching и UI в одном сервисе.
- Не использовать глобальные mutable singleton-состояния для сессии.
- Делать операции Start, Pause, Stop и Reset идемпотентными.
- Обрабатывать отмену и завершение процессов.
- Не оставлять фоновые listeners после закрытия окна или смены сессии.
- Комментариями объяснять причину нетривиального решения, а не пересказывать код.
- Не проводить несвязанный рефакторинг вместе с продуктовой задачей.
Обязательные проверки
Для trigger/index-логики покрыть тестами как минимум:
- точное совпадение;
- разные варианты одной фразы;
ёие;- повтор одного partial transcript;
- partial, который затем стал final;
- два настоящих произнесения с паузой;
- пересекающиеся правила;
- слово как часть другого слова;
- cooldown;
- достижение и превышение 100;
- Reset.
Для аудио проверить:
- Start → Listening;
- Pause действительно останавливает tracks;
- смену устройства;
- исчезновение устройства;
- падение recognizer;
- перезапуск utility process;
- игнорирование событий старой сессии;
- ограничение очереди;
- отсутствие сохранённых аудиофайлов.
Для UI проверить:
- все шесть уровней лица;
- transient-реакцию;
- состояния разрешений и ошибок;
- работу при выключенной анимации;
- отсутствие зависания при медленном recognizer.
Рекомендуемый порядок реализации
- Создать Electron scaffold и чистые domain-модули с unit-тестами.
- Собрать первый сквозной demo-срез на mock recognizer: Start → тестовое utterance → индекс → реакция лица.
- Подключить микрофон, AudioWorklet, session lifecycle и mock inference в utility process.
- Заменить mock на локальный
sherpa-onnxadapter и проверить качество на реальных фразах. - Добавить редактирование rules, выбор устройства, tray и нормальные error states.
- Собрать установщики и пройти release-проверки на каждой заявленной платформе.
- Только затем добавлять system loopback отдельными platform providers.
Как агент должен работать
- Сначала прочитать этот файл и существующий код.
- Кратко сформулировать, какой пользовательский результат будет получен.
- Назвать основные файлы, которых коснётся изменение.
- Выбрать самый маленький законченный вертикальный срез.
- Сохранить существующие решения, если нет конкретной причины менять их.
- Реализовать изменение.
- Запустить релевантные проверки.
- Не заявлять о проверке платформы, на которой тесты не запускались.
- В конце дать короткую человеческую сводку.
Формат итоговой сводки:
Готово
- Что теперь умеет пользователь.
Изменено
- Путь к файлу — зачем он изменён.
Проверено
- Какие команды или сценарии прошли.
Осталось
- Только реальные ограничения или следующий логичный шаг.
Не писать полотно о каждой строке. Объяснять важные решения обычным языком, учитывая, что пользователь — опытный разработчик, которому нужна короткая и хорошо структурированная сводка.
Навыки проекта
$build-max-index— общая разработка и выбор вертикального среза.$implement-max-index-audio— микрофон, системный звук, AudioWorklet, recognizer и разрешения.$implement-max-index-experience— правила фраз, индекс, HUD и реакции персонажа.$verify-max-index— тестирование, диагностика и проверка готовности релиза.