424 lines
22 KiB
Markdown
424 lines
22 KiB
Markdown
# Max Index — контракт для агента
|
||
|
||
## Что мы создаём
|
||
|
||
Max Index — небольшое кроссплатформенное desktop-приложение для дейликов и других разговоров.
|
||
|
||
Приложение:
|
||
|
||
1. Слушает выбранный источник звука.
|
||
2. Локально распознаёт русскую речь.
|
||
3. Находит заранее настроенные фразы вроде «это сделать легко».
|
||
4. Увеличивает индекс от 0 до 100.
|
||
5. Немедленно меняет лицо персонажа и показывает короткую реакцию.
|
||
|
||
Продукт должен ощущаться как смешной живой индикатор встречи, а не как тяжёлая система транскрибации.
|
||
|
||
## Какой результат хотим получить
|
||
|
||
Первая полноценная версия должна:
|
||
|
||
- работать на 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`.
|
||
|
||
## Архитектура
|
||
|
||
Разделять приложение на процессы и чистую предметную логику:
|
||
|
||
```text
|
||
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;
|
||
- вычисление индекса;
|
||
- выбор визуального состояния.
|
||
|
||
## Предлагаемая структура
|
||
|
||
```text
|
||
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:
|
||
|
||
```ts
|
||
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.
|
||
|
||
Распознавание должно работать локально. Любой облачный вариант добавлять только как явную необязательную функцию после согласования.
|
||
|
||
## Контракт правил и индекса
|
||
|
||
Минимальная модель правила:
|
||
|
||
```ts
|
||
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 не сработал повторно.
|
||
|
||
Индекс изменять только через чистую функцию:
|
||
|
||
```ts
|
||
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.
|
||
|
||
## Рекомендуемый порядок реализации
|
||
|
||
1. Создать Electron scaffold и чистые domain-модули с unit-тестами.
|
||
2. Собрать первый сквозной demo-срез на mock recognizer: Start → тестовое utterance → индекс → реакция лица.
|
||
3. Подключить микрофон, AudioWorklet, session lifecycle и mock inference в utility process.
|
||
4. Заменить mock на локальный `sherpa-onnx` adapter и проверить качество на реальных фразах.
|
||
5. Добавить редактирование rules, выбор устройства, tray и нормальные error states.
|
||
6. Собрать установщики и пройти release-проверки на каждой заявленной платформе.
|
||
7. Только затем добавлять system loopback отдельными platform providers.
|
||
|
||
## Как агент должен работать
|
||
|
||
1. Сначала прочитать этот файл и существующий код.
|
||
2. Кратко сформулировать, какой пользовательский результат будет получен.
|
||
3. Назвать основные файлы, которых коснётся изменение.
|
||
4. Выбрать самый маленький законченный вертикальный срез.
|
||
5. Сохранить существующие решения, если нет конкретной причины менять их.
|
||
6. Реализовать изменение.
|
||
7. Запустить релевантные проверки.
|
||
8. Не заявлять о проверке платформы, на которой тесты не запускались.
|
||
9. В конце дать короткую человеческую сводку.
|
||
|
||
Формат итоговой сводки:
|
||
|
||
```text
|
||
Готово
|
||
- Что теперь умеет пользователь.
|
||
|
||
Изменено
|
||
- Путь к файлу — зачем он изменён.
|
||
|
||
Проверено
|
||
- Какие команды или сценарии прошли.
|
||
|
||
Осталось
|
||
- Только реальные ограничения или следующий логичный шаг.
|
||
```
|
||
|
||
Не писать полотно о каждой строке. Объяснять важные решения обычным языком, учитывая, что пользователь — опытный разработчик, которому нужна короткая и хорошо структурированная сводка.
|
||
|
||
## Навыки проекта
|
||
|
||
- `$build-max-index` — общая разработка и выбор вертикального среза.
|
||
- `$implement-max-index-audio` — микрофон, системный звук, AudioWorklet, recognizer и разрешения.
|
||
- `$implement-max-index-experience` — правила фраз, индекс, HUD и реакции персонажа.
|
||
- `$verify-max-index` — тестирование, диагностика и проверка готовности релиза.
|