Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QKHeGuAYh4W1jNdjMConZa
127 lines
13 KiB
Markdown
127 lines
13 KiB
Markdown
# recording-lifecycle Specification
|
||
|
||
## Purpose
|
||
Гарантирует, что каждая завершённая или прерванная голосовая запись сохраняется немедленно и доводится до текста через отправку во внешний сервис с повторами, без дублей между устройствами и без потери данных.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Состояния записи
|
||
|
||
Запись MUST находиться ровно в одном из состояний: `created` (идёт запись, данные только в памяти), `saved` (аудио и заметка сохранены, ожидает отправки), `sent` (запрос в полёте), `transcribed` (текст получен, не записан; не сохраняется), `retry` (ожидает повтора после временной ошибки), `failed` (требуется ручное действие), `inserted` (финал). Во frontmatter заметки MUST сохраняться только `saved`, `sent`, `retry`, `failed`, `inserted`. Переход, не описанный в этой спецификации, MUST отклоняться ошибкой без изменения записи.
|
||
|
||
#### Scenario: Недопустимый переход отклонён
|
||
- **WHEN** к записи в состоянии `inserted` применяется событие «отправка начата»
|
||
- **THEN** операция завершается ошибкой, запись не изменена
|
||
|
||
#### Scenario: Транзитное состояние не попадает в заметку
|
||
- **WHEN** получен текст транскрипта
|
||
- **THEN** заметка за одну запись переходит из `sent` сразу в `inserted`, значение `transcribed` во frontmatter не появляется
|
||
|
||
### Requirement: Сохранение захваченного аудио
|
||
|
||
По окончании записи плагин MUST сохранить аудио в `voice/source/` и создать заметку в `voice/inbox/` в состоянии `saved` до любой сетевой операции. Если захвачено 0 байт, MUST NOT создаваться ни аудиофайл, ни заметка. Если запись была прервана (событие остановки не от пользователя), сохраняется всё накопленное, а запись помечается как прерванная.
|
||
|
||
#### Scenario: Обычная запись
|
||
- **WHEN** пользователь остановил запись длиной 0:47 с непустыми данными в формате `audio/mp4`
|
||
- **THEN** в `voice/source/` появился файл `<id>.m4a`, в `voice/inbox/` — заметка со статусом `saved`, и это произошло без сетевых запросов
|
||
|
||
#### Scenario: Прерванная запись
|
||
- **WHEN** запись прервалась системой после 0:47 с непустыми данными
|
||
- **THEN** аудио и заметка сохранены, заметка помечена как прерванная с длительностью 0:47, и запись участвует в очереди как обычная
|
||
|
||
#### Scenario: Пустая запись
|
||
- **WHEN** запись завершилась (штатно или обрывом) с 0 байт данных
|
||
- **THEN** никаких файлов не создано
|
||
|
||
### Requirement: Отправка и результат
|
||
|
||
Перед отправкой запись MUST перейти в `sent` с отметкой времени начала отправки, записанной в заметку. Очередь MUST обрабатывать записи строго по одной. Результат провайдера — успех с текстом, временная ошибка или постоянная ошибка. Успех с непустым текстом MUST переводить запись в `inserted` с текстом в заметке. Успех с пустым (после обрезки пробелов) текстом MUST переводить запись в `failed` с причиной «нет речи»: это свойство записи, а не ошибка сервиса, повтор бесполезен.
|
||
|
||
#### Scenario: Успешное распознавание
|
||
- **WHEN** провайдер вернул текст, а запись в момент ответа в `sent`
|
||
- **THEN** заметка содержит текст и статус `inserted`
|
||
|
||
#### Scenario: В записи нет речи
|
||
- **WHEN** провайдер вернул успех с текстом из одних пробелов
|
||
- **THEN** запись в `failed` с причиной «нет речи», в заметке — «В записи не распознано речи»
|
||
|
||
#### Scenario: Последовательная обработка
|
||
- **WHEN** в очереди три готовые к отправке записи
|
||
- **THEN** второй запрос к провайдеру начинается только после завершения первого
|
||
|
||
### Requirement: Повторы после временных ошибок
|
||
|
||
Временная ошибка MUST увеличивать счётчик попыток и переводить запись в `retry` со временем следующей попытки через 30 с, 2 мин, 10 мин и 30 мин после 1-й, 2-й, 3-й и 4-й неудачи соответственно. Пятая неудача подряд MUST переводить запись в `failed`. Постоянная ошибка MUST переводить запись в `failed` сразу, независимо от счётчика. Запись в `retry` MUST NOT отправляться раньше своего времени следующей попытки.
|
||
|
||
#### Scenario: Сеть пропала и вернулась
|
||
- **WHEN** первая отправка завершилась временной ошибкой в 14:15:03, а следующая попытка после 14:15:33 — успехом
|
||
- **THEN** после первой попытки запись в `retry` со следующей попыткой 14:15:33, после второй — в `inserted`
|
||
|
||
#### Scenario: Исчерпание попыток
|
||
- **WHEN** пять отправок подряд завершились временной ошибкой
|
||
- **THEN** запись в `failed`, причина — последняя ошибка
|
||
|
||
#### Scenario: Постоянная ошибка
|
||
- **WHEN** первая отправка завершилась постоянной ошибкой «401 Unauthorized»
|
||
- **THEN** запись сразу в `failed` с этой причиной
|
||
|
||
#### Scenario: Время повтора не наступило
|
||
- **WHEN** очередь запускается в 14:15:20 для записи со следующей попыткой 14:15:33
|
||
- **THEN** запрос к провайдеру не отправляется
|
||
|
||
### Requirement: Восстановление после прерванной отправки
|
||
|
||
Запись текущего устройства в `sent`, у которой с начала отправки прошло больше 10 минут, MUST считаться завершившейся временной ошибкой «отправка прервана» и обрабатываться по правилам повторов.
|
||
|
||
#### Scenario: Obsidian выгружен во время запроса
|
||
- **WHEN** очередь запускается в 14:30:00 и находит запись в `sent` с началом отправки 14:19:00
|
||
- **THEN** запись учитывается как временная неудача и планируется к повтору
|
||
|
||
#### Scenario: Запрос ещё может идти
|
||
- **WHEN** очередь находит запись в `sent` с началом отправки 2 минуты назад
|
||
- **THEN** запись не трогается
|
||
|
||
### Requirement: Принадлежность устройству
|
||
|
||
Каждая запись MUST хранить идентификатор создавшего её устройства. Автоматическая обработка очереди MUST брать только записи текущего устройства. Идентификатор устройства MUST NOT синхронизироваться между устройствами.
|
||
|
||
#### Scenario: Чужая запись не отправляется автоматически
|
||
- **WHEN** на маке запускается очередь, а в vault есть запись в `saved`, созданная на iPhone
|
||
- **THEN** мак не отправляет её
|
||
|
||
### Requirement: Команда «Распознать все ожидающие здесь»
|
||
|
||
Команда MUST переназначить текущему устройству все записи других устройств в состояниях `saved`, `retry` и `sent` старше 10 минут, после чего обработать очередь без ожидания времени следующей попытки. Записи в `failed` и `inserted`, а также `sent` не старше 10 минут, MUST NOT затрагиваться.
|
||
|
||
#### Scenario: Мак забирает запись телефона
|
||
- **WHEN** на маке выполнена команда, а в vault есть запись iPhone в `retry` со следующей попыткой через 20 минут
|
||
- **THEN** запись переназначена маку и отправлена сейчас
|
||
|
||
### Requirement: Команда «Повторить распознавание»
|
||
|
||
Команда для записи в `failed` или `retry` MUST сбросить счётчик попыток в 0, перевести запись в `saved` и запустить очередь. Для записей в других состояниях команда MUST ничего не менять.
|
||
|
||
#### Scenario: Повтор после исправления ключа
|
||
- **WHEN** запись в `failed` после 401, пользователь исправил ключ и выполнил команду
|
||
- **THEN** запись отправлена заново со счётчиком попыток, начатым с нуля
|
||
|
||
### Requirement: Удалённая заметка отменяет запись
|
||
|
||
Состояние записи живёт только во frontmatter её заметки; очередь находит записи по наличию ключа `voice-id` в любом месте vault, а не по папке. Если заметку удалили, запись MUST считаться отменённой, и её аудиофайл MUST NOT удаляться. Если заметку удалили во время запроса, пришедший результат MUST отбрасываться без ошибки и без воссоздания заметки.
|
||
|
||
#### Scenario: Заметка удалена во время запроса
|
||
- **WHEN** провайдер вернул текст после того, как заметку удалили
|
||
- **THEN** заметка не создана заново, очередь продолжает работу со следующей записью
|
||
|
||
#### Scenario: Заметка перенесена в другую папку
|
||
- **WHEN** пользователь перенёс заметку в `saved` из `voice/inbox/` в `projects/`
|
||
- **THEN** запись по-прежнему обрабатывается очередью
|
||
|
||
### Requirement: Повторный запуск очереди
|
||
|
||
Если запуск очереди запрошен, пока она уже работает, новый параллельный проход MUST NOT начинаться; вместо этого MUST выполниться ровно один дополнительный проход после завершения текущего.
|
||
|
||
#### Scenario: Три запроса во время работы
|
||
- **WHEN** очередь обрабатывает запись и за это время запуск запрошен три раза
|
||
- **THEN** после текущего прохода выполняется ровно один дополнительный проход
|