# 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/` появился файл `.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** после текущего прохода выполняется ровно один дополнительный проход