Andrey Epifancev 9b4ea0a3c9 docs(openspec): archive core-recording-lifecycle, promote specs (#1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKHeGuAYh4W1jNdjMConZa
2026-09-27 19:26:25 +04:00

7.4 KiB
Raw Blame History

voice-note-format Specification

Purpose

Определяет, где и под какими именами лежат аудиофайлы и заметки голосовых записей, как в заметке хранится состояние записи и как плагин обновляет заметку, не затрагивая текст пользователя.

Requirements

Requirement: Идентификатор и имена файлов

Идентификатор записи MUST иметь вид YYYYMMDD-HHMMSS-xxxx (время начала записи в UTC и 4 случайных шестнадцатеричных символа в нижнем регистре). Аудио MUST сохраняться как voice/source/<id>.<ext>, где расширение определяется по mime без параметров: audio/mp4 → m4a, audio/webm → webm, audio/ogg → ogg, любой другой → bin. Заметка MUST создаваться как voice/inbox/YYYY-MM-DD HH-mm-ss.md по локальному времени начала записи; при занятом имени MUST добавляться суффикс (2), (3) и т.д.

Scenario: Mime с параметрами

  • WHEN запись в формате audio/webm;codecs=opus начата 27.09.2026 в 14:15:03 на устройстве в поясе UTC+3
  • THEN аудио сохранено как voice/source/20260927-111503-<4 hex>.webm, заметка — voice/inbox/2026-09-27 14-15-03.md

Scenario: Коллизия имени заметки

  • WHEN заметка voice/inbox/2026-09-27 14-15-03.md уже существует
  • THEN новая заметка создана как voice/inbox/2026-09-27 14-15-03 (2).md

Requirement: Ключи frontmatter

Состояние записи MUST храниться в плоских ключах frontmatter: voice-id, voice-status, voice-device, voice-audio (wiki-ссылка на аудио), voice-mime (исходный mime с параметрами), voice-attempts, а также, когда применимо, voice-next-attempt и voice-sent-at (ISO 8601 UTC), voice-error, voice-interrupted. Плагин MUST изменять только ключи с префиксом voice-, а остальные ключи и их порядок MUST оставаться нетронутыми. При переходе в inserted ключи voice-next-attempt, voice-sent-at и voice-error MUST удаляться. Заметка с отсутствующим или некорректным voice-status MUST игнорироваться очередью.

Scenario: Пользовательские свойства сохраняются

  • WHEN пользователь добавил в заметку свойство tags: [idea], а затем запись перешла в inserted
  • THEN tags: [idea] на месте в прежней позиции, ключи voice-* обновлены

Scenario: Испорченный статус

  • WHEN пользователь вручную записал voice-status: foo
  • THEN очередь не обрабатывает заметку и не падает

Requirement: Тело заметки до распознавания

Новая заметка MUST содержать вложение аудио ![[<путь к аудио>]], затем управляемую область, ограниченную маркерами-комментариями %%voice-note:<id>%% и %%/voice-note%%. Внутри области MUST отображаться состояние: для saved/sent — > [!info] Распознаётся…; для retry — > [!info] Не удалось отправить, повтор в HH:mm (попытка N/5) по локальному времени; для failed — > [!warning] Не распознано: <причина>. Команда «Повторить распознавание», кроме причины «нет речи», для которой — > [!info] В записи не распознано речи (без слова «ошибка» и без призыва повторить). Для прерванной записи над областью MUST стоять строка > [!note] Запись прервана, сохранено M:SS, которая остаётся и после вставки текста.

Scenario: Заметка в ожидании повтора

  • WHEN запись перешла в retry после второй неудачи со следующей попыткой в 14:17 по местному времени
  • THEN область содержит > [!info] Не удалось отправить, повтор в 14:17 (попытка 2/5)

Requirement: Вставка транскрипта

При переходе в inserted управляемая область вместе с маркерами MUST заменяться текстом транскрипта. Весь текст за пределами области MUST оставаться без изменений. Если маркеры не найдены, транскрипт MUST дописываться в конец заметки через пустую строку. Если запись уже в inserted, повторная вставка MUST не менять заметку. Обновление статуса и тела MUST выполняться одной операцией записи.

Scenario: Пользователь дописал текст под заметкой

  • WHEN под управляемой областью пользователь написал «купить молоко», затем пришёл транскрипт
  • THEN область заменена транскриптом, «купить молоко» осталось на месте

Scenario: Маркеры удалены вручную

  • WHEN пользователь стёр маркеры, затем пришёл транскрипт
  • THEN транскрипт дописан в конец заметки, статус inserted

Scenario: Дубль ответа

  • WHEN транскрипт приходит для записи, уже находящейся в inserted
  • THEN содержимое заметки не изменилось

Requirement: Golden-эталоны

Для каждой пары фикстур tests/fixtures/<name>.<audio-ext> + <name>.txt с эталоном <name>.note.md полный путь «захват → очередь → вставка» с провайдером, отвечающим содержимым <name>.txt, при фиксированных времени, идентификаторе устройства и источнике случайности MUST давать заметку, побайтно совпадающую с <name>.note.md. Провайдер MUST получать байты аудиофикстуры без изменений и исходный mime.

Scenario: test-01

  • WHEN прогоняется фикстура test-01.m4a с mime audio/mp4 и ответом из test-01.txt
  • THEN итоговая заметка совпадает с test-01.note.md