Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QKHeGuAYh4W1jNdjMConZa
63 lines
7.4 KiB
Markdown
63 lines
7.4 KiB
Markdown
# 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`
|