docs(openspec): archive openai-transcriber, promote transcription-provider spec (#5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d2e7659ae8
commit
9144427bba
133
openspec/specs/transcription-provider/spec.md
Normal file
133
openspec/specs/transcription-provider/spec.md
Normal file
@ -0,0 +1,133 @@
|
||||
# transcription-provider Specification
|
||||
|
||||
## Purpose
|
||||
Определяет, как плагин отправляет аудиозапись в OpenAI-совместимый сервис распознавания (по умолчанию AITUNNEL, `gpt-4o-transcribe`), как раскладывает любой исход в результат `ok` / `temporary` / `permanent` для очереди и как хранит настройки провайдера, не допуская утечки API-ключа.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Настройки провайдера
|
||||
|
||||
Провайдер MUST использовать настройки `baseUrl`, `model`, `apiKey`, `language` со значениями по умолчанию `https://api.aitunnel.ru/v1`, `gpt-4o-transcribe`, пустая строка и пустая строка соответственно. Настройки MUST храниться только в `data.json` плагина и MUST читаться заново при каждом запросе, так что изменённый ключ применяется к следующему запросу без перезапуска плагина. Значения `baseUrl`, `model`, `apiKey` и `language` MUST обрезаться от пробелов при чтении. Сохранённые настройки MUST накладываться поверх значений по умолчанию, чтобы отсутствующее поле получало умолчание. API-ключ MUST NOT попадать в репозиторий: ни в исходники, ни в тесты, ни в фикстуры.
|
||||
|
||||
#### Scenario: Первый запуск
|
||||
- **WHEN** плагин загружается, а `data.json` отсутствует
|
||||
- **THEN** провайдер использует `https://api.aitunnel.ru/v1`, модель `gpt-4o-transcribe`, пустой ключ и пустой язык
|
||||
|
||||
#### Scenario: Ключ исправлен между запросами
|
||||
- **WHEN** запрос завершился ошибкой 401, пользователь ввёл новый ключ во вкладке настроек, и провайдер вызван снова (очередью или командой проверки)
|
||||
- **THEN** следующий запрос уходит с новым ключом без перезапуска плагина
|
||||
|
||||
#### Scenario: Ключ вставлен с пробелами
|
||||
- **WHEN** в настройке ключа сохранено `" sk-abc \n"`
|
||||
- **THEN** запрос уходит с заголовком `Authorization: Bearer sk-abc`
|
||||
|
||||
### Requirement: Вкладка настроек
|
||||
|
||||
Плагин MUST предоставлять вкладку настроек с полями «Адрес сервиса» (`baseUrl`), «Модель», «API-ключ» и «Язык». Поле ключа MUST отображаться как поле пароля. Изменение любого поля MUST сохраняться в `data.json`.
|
||||
|
||||
#### Scenario: Ввод ключа
|
||||
- **WHEN** пользователь вводит ключ во вкладке настроек
|
||||
- **THEN** символы ключа скрыты, а после ввода ключ сохранён в `data.json`
|
||||
|
||||
### Requirement: Формирование запроса
|
||||
|
||||
Провайдер MUST отправлять один POST-запрос на `<baseUrl>/audio/transcriptions`, где у `baseUrl` отброшены завершающие `/`. Запрос MUST содержать заголовок `Authorization: Bearer <apiKey>` и тело `multipart/form-data` с полями `model`, `response_format` со значением `json` и `file`. Поле `language` MUST передаваться только если настройка не пустая. Часть `file` MUST иметь имя `audio.<ext>`, где расширение определяется по mime записи (`audio/mp4` → `m4a`, `audio/webm` → `webm`, `audio/ogg` → `ogg`, иначе `bin`), и `Content-Type`, равный mime записи без параметров. Байты аудио MUST передаваться без изменений. Тип тела `multipart/form-data; boundary=<boundary>` MUST передаваться HTTP-клиенту отдельным полем `contentType`, а boundary в нём MUST совпадать с boundary тела.
|
||||
|
||||
#### Scenario: Запись с iPhone
|
||||
- **WHEN** отправляется запись с mime `audio/mp4;codecs=mp4a.40.2`, `baseUrl` `https://api.aitunnel.ru/v1/`, язык не задан
|
||||
- **THEN** запрос уходит на `https://api.aitunnel.ru/v1/audio/transcriptions` с полями `model=gpt-4o-transcribe`, `response_format=json` и файлом `audio.m4a` с `Content-Type: audio/mp4`, поля `language` нет
|
||||
|
||||
#### Scenario: Язык задан
|
||||
- **WHEN** в настройках язык `ru`
|
||||
- **THEN** в запросе есть поле `language=ru`
|
||||
|
||||
### Requirement: Проверки до запроса
|
||||
|
||||
Провайдер MUST завершать распознавание без сетевого запроса в следующих случаях, проверяемых в этом порядке: пустой `apiKey` — постоянная ошибка «не задан API-ключ»; пустой `baseUrl` — постоянная ошибка «не задан адрес сервиса»; аудио длиной 0 байт — постоянная ошибка «пустая запись»; уже отменённый сигнал — временная ошибка «запрос отменён».
|
||||
|
||||
#### Scenario: Ключ не задан
|
||||
- **WHEN** очередь отправляет запись, а ключ в настройках пустой
|
||||
- **THEN** сетевого запроса нет, результат — постоянная ошибка «не задан API-ключ»
|
||||
|
||||
#### Scenario: Пустое аудио
|
||||
- **WHEN** провайдеру передано аудио длиной 0 байт
|
||||
- **THEN** сетевого запроса нет, результат — постоянная ошибка «пустая запись»
|
||||
|
||||
### Requirement: Классификация ответа
|
||||
|
||||
Провайдер MUST раскладывать исход запроса так:
|
||||
|
||||
| Исход | Результат | Причина |
|
||||
|---|---|---|
|
||||
| 2xx, тело — JSON со строковым полем `text` | успех с этим текстом без изменений | — |
|
||||
| 2xx, тело не JSON или без строкового поля `text` | постоянная ошибка | `HTTP <код>: ответ без поля text: <первые 200 символов тела>` |
|
||||
| 408, 429, 5xx | временная ошибка | `HTTP <код>: <деталь>` |
|
||||
| 3xx и 4xx, кроме 408 и 429 | постоянная ошибка | `HTTP <код>: <деталь>` |
|
||||
| 1xx и любой другой код | временная ошибка | `HTTP <код>: <деталь>` |
|
||||
| HTTP-клиент бросил исключение | временная ошибка | `сеть: <сообщение исключения>` |
|
||||
|
||||
Деталь MUST браться из `error.message`, если тело — JSON вида `{"error":{"message":"…"}}` со строковым `message`, иначе MUST быть первыми 200 символами тела. Если тело пустое или из одних пробелов, MUST использоваться причина `HTTP <код>` без двоеточия и детали. Провайдер MUST NOT бросать исключения: любой исход MUST возвращаться как результат.
|
||||
|
||||
#### Scenario: Неверный ключ
|
||||
- **WHEN** сервис ответил 401 с телом `{"error":{"message":"Invalid API key"}}`
|
||||
- **THEN** результат — постоянная ошибка `HTTP 401: Invalid API key`
|
||||
|
||||
#### Scenario: Превышен лимит запросов
|
||||
- **WHEN** сервис ответил 429
|
||||
- **THEN** результат — временная ошибка, очередь планирует повтор
|
||||
|
||||
#### Scenario: Неверный адрес сервиса
|
||||
- **WHEN** сервис ответил 200 с HTML-страницей вместо JSON
|
||||
- **THEN** результат — постоянная ошибка, причина начинается с `HTTP 200: ответ без поля text:` и содержит не больше 200 символов тела
|
||||
|
||||
#### Scenario: Редирект дошёл до провайдера
|
||||
- **WHEN** HTTP-клиент вернул статус 301
|
||||
- **THEN** результат — постоянная ошибка
|
||||
|
||||
#### Scenario: Сеть недоступна
|
||||
- **WHEN** HTTP-клиент бросил исключение `net::ERR_INTERNET_DISCONNECTED`
|
||||
- **THEN** результат — временная ошибка `сеть: net::ERR_INTERNET_DISCONNECTED`
|
||||
|
||||
#### Scenario: Пустой текст
|
||||
- **WHEN** сервис ответил 200 с телом `{"text":" "}`
|
||||
- **THEN** результат — успех с текстом `" "`, а решение «нет речи» принимает очередь по `recording-lifecycle`
|
||||
|
||||
### Requirement: Таймаут и отмена
|
||||
|
||||
Провайдер MUST завершать ожидание ответа через 5 минут временной ошибкой «таймаут запроса (5 мин)». Таймаут MUST быть меньше порога зависшей отправки из `recording-lifecycle` (10 минут). Если сигнал отмены срабатывает во время запроса, провайдер MUST сразу вернуть временную ошибку «запрос отменён». После возврата результата провайдер MUST NOT оставлять активных таймеров и слушателей сигнала. Ответ или исключение запроса, пришедшие после таймаута или отмены, MUST игнорироваться без необработанного отклонения промиса.
|
||||
|
||||
#### Scenario: Сервис молчит
|
||||
- **WHEN** ответ на запрос не пришёл за 5 минут
|
||||
- **THEN** результат — временная ошибка «таймаут запроса (5 мин)», очередь планирует повтор
|
||||
|
||||
#### Scenario: Плагин выгружен во время запроса
|
||||
- **WHEN** очередь отменяет сигнал, пока запрос в полёте
|
||||
- **THEN** провайдер сразу возвращает временную ошибку «запрос отменён», а пришедший позже ответ ни на что не влияет
|
||||
|
||||
### Requirement: Причина ошибки без секретов
|
||||
|
||||
Причина любой ошибки MUST NOT содержать API-ключ, значение заголовка `Authorization` и какую-либо часть тела запроса. Перед возвратом каждая причина MUST проходить очистку: каждое вхождение ключа заменяется на `***`, каждая последовательность `Bearer <непробельные символы>` заменяется на `Bearer ***`, результат обрезается до 300 символов.
|
||||
|
||||
#### Scenario: Сервис вернул ключ в тексте ошибки
|
||||
- **WHEN** сервис ответил 401 с телом, содержащим ключ и строку `Authorization: Bearer <ключ>`
|
||||
- **THEN** причина не содержит ключа, вместо него `***`
|
||||
|
||||
#### Scenario: Ключ в сообщении сетевого исключения
|
||||
- **WHEN** HTTP-клиент бросил исключение, в сообщении которого есть ключ
|
||||
- **THEN** причина не содержит ключа
|
||||
|
||||
#### Scenario: Тело запроса не попадает в причину
|
||||
- **WHEN** запрос завершился любой ошибкой
|
||||
- **THEN** причина не содержит boundary тела и строки `Content-Disposition`
|
||||
|
||||
### Requirement: Команда «Проверить провайдер распознавания»
|
||||
|
||||
Команда MUST отправить провайдеру последний по имени аудиофайл из `voice/source/` с расширением `m4a`, `webm` или `ogg`, с mime по расширению, и показать исход во всплывающем уведомлении: при успехе — начало распознанного текста или сообщение, что речь не распознана; при ошибке — вид ошибки (временная или постоянная) и её причину. Если подходящего файла нет, команда MUST показать уведомление об этом без сетевого запроса. Команда MUST NOT изменять заметки, frontmatter и очередь.
|
||||
|
||||
#### Scenario: Проверка после смены ключа
|
||||
- **WHEN** в `voice/source/` есть записи, пользователь выполнил команду
|
||||
- **THEN** последняя запись отправлена провайдеру, уведомление показывает начало текста, ни одна заметка не изменилась
|
||||
|
||||
#### Scenario: Нет записей
|
||||
- **WHEN** папка `voice/source/` пуста или отсутствует
|
||||
- **THEN** уведомление сообщает, что проверять нечем, сетевого запроса нет
|
||||
Loading…
x
Reference in New Issue
Block a user