внутренняя модель перестроена вокруг аудиозаписи
- audiorecords вместо transcribe_jobs: приложения (texts, structures, recognitions, record_events, topics) живут своими коллекциями, ссылки на исходник и на приведённую копию перестали переставляться - рубеж называет достигнутое, отказ стал признаком остановки с причиной, а сторожей стало двое: число отказов и время в рубеже - воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Приём записи по HTTP
|
||||
|
||||
Сервис SHALL принимать запись от внешней программы запросом `POST /api/audio` с
|
||||
телом `multipart/form-data` и полем `audio` **только от узнанного отправителя**.
|
||||
Запрос без сессии MUST получать код `401`, и по нему MUST не заводиться ни файл,
|
||||
ни аудиозапись. Принятая запись от узнанного отправителя MUST быть сохранена и
|
||||
получить заведённую под неё аудиозапись на рубеже `uploaded`; ответ MUST нести
|
||||
идентификатор записи полем `job_id` и её рубеж полем `status`.
|
||||
|
||||
Значение рубежа в ответе изменилось: прежде приём отдавал `created`. Перечень
|
||||
состояний назван проектом необратимым, и ломка объявлена прямо — состояние
|
||||
теперь называет достигнутое, а не предстоящее, и `created` в новом перечне нет
|
||||
вовсе.
|
||||
|
||||
Имена полей ответа нормативны и MUST остаться прежними: контракт HTTP API
|
||||
объявлен проектом необратимым, и переименование поля ломает внешнюю программу
|
||||
молча. Меняются значения поля рубежа, а не его имя.
|
||||
|
||||
Отказ по отсутствию сессии наступает **раньше** чтения тела: запись, за которую
|
||||
не заплатит узнанный отправитель, не должна попасть даже в память.
|
||||
|
||||
Приём не судит о годности записи сам: расширение он берёт из имени файла, а
|
||||
пригодность содержимого узнаёт у источника метаданных.
|
||||
|
||||
Куда именно ложится принятая запись, приёму не принадлежит: раскладку выбирает
|
||||
хранилище, и нормирует её capability `storage`.
|
||||
|
||||
Владельцем принятой записи приём SHALL назначать предъявителя сессии. Проверка
|
||||
стоит здесь, а не только в схеме хранилища: колонка владельца допускает пустое
|
||||
значение ради записей из Telegram, и приём по HTTP — то место, где
|
||||
обязательность держится.
|
||||
|
||||
Предъявитель, чья сессия не даёт учётной записи пользователя, MUST получать
|
||||
отказ `403` и MUST получать его **до чтения тела** — там же, где стоит отказ по
|
||||
отсутствию сессии. Сессия владельца панели — именно такой случай: узнан он всё
|
||||
же узнан, а записи в коллекции пользователей у него нет, и владельцем записи он
|
||||
стать не может.
|
||||
|
||||
Код здесь другой, чем у запроса без сессии, и это не оплошность: `401` значит
|
||||
«предъяви себя», а предъявитель себя предъявил. Утечки по разнице кодов нет —
|
||||
оба ответа говорят о самом спрашивающем, а не о том, какие записи заведены.
|
||||
|
||||
Отказ **после** укладки записи потребовал бы убрать уже сохранённый файл, а
|
||||
уборки файлов сервис не умеет вовсе: норма, обязывающая к недостижимому, не
|
||||
пишется.
|
||||
|
||||
#### Scenario: Запись принята
|
||||
|
||||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||||
- **AND** отправитель предъявил сессию
|
||||
- **WHEN** программа шлёт `POST /api/audio` с полем `audio`
|
||||
- **THEN** ответ имеет код `201`, а в теле лежат непустой `job_id` и `status`
|
||||
со значением `uploaded`
|
||||
- **AND** содержимое записи целиком лежит в хранилище одним файлом
|
||||
- **AND** владельцем заведённой аудиозаписи стоит предъявитель сессии
|
||||
|
||||
#### Scenario: Сессия не даёт учётной записи пользователя
|
||||
|
||||
- **GIVEN** предъявлена сессия владельца панели
|
||||
- **WHEN** он шлёт `POST /api/audio` с полем `audio`
|
||||
- **THEN** ответ имеет код `403`
|
||||
- **AND** ни файла, ни аудиозаписи не заводится
|
||||
|
||||
#### Scenario: Сессии нет
|
||||
|
||||
- **WHEN** программа шлёт `POST /api/audio` с полем `audio` без сессии
|
||||
- **THEN** ответ имеет код `401`
|
||||
- **AND** ни файла, ни аудиозаписи не заводится
|
||||
- **AND** тело ответа не несёт данных записи
|
||||
|
||||
#### Scenario: Поля с записью нет
|
||||
|
||||
- **GIVEN** отправитель предъявил сессию
|
||||
- **WHEN** программа шлёт `POST /api/audio` без поля `audio`
|
||||
- **THEN** ответ имеет код `400` и сообщение об отсутствии записи
|
||||
- **AND** ни файла, ни аудиозаписи не заводится
|
||||
|
||||
#### Scenario: Размеру записи приём не судья
|
||||
|
||||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||||
- **AND** отправитель предъявил сессию
|
||||
- **WHEN** программа шлёт запись нулевой длины
|
||||
- **THEN** ответ имеет код `201`: собственного порога по размеру у приёма нет
|
||||
|
||||
### Requirement: Опрос готовности задачи
|
||||
|
||||
Сервис SHALL отдавать рубеж аудиозаписи по запросу `GET /api/status/:id`
|
||||
**только её владельцу**. Запрос без сессии MUST получать код `401`, и тело
|
||||
такого ответа MUST не нести ни рубежа записи, ни текста расшифровки. Ответ
|
||||
владельцу MUST нести идентификатор полем `job_id`, рубеж полем `status` и время
|
||||
заведения полем `created_at`, а текст расшифровки полем `transcription_text`, и
|
||||
это поле MUST отсутствовать в ответе, пока текста нет: пустая строка на месте
|
||||
отсутствующего текста читается как «расшифровка пуста».
|
||||
|
||||
Видов текста у записи больше одного, поэтому ответ MUST называть вид, который
|
||||
отдаёт: в поле `transcription_text` уходит **сырая расшифровка**, и только она.
|
||||
Вычитанный текст этим полем MUST не подменяться — иначе значение поля менялось бы
|
||||
у одной и той же записи от того, успел ли отработать необязательный шаг, а
|
||||
контракт объявлен необратимым. Отдача «последнего записанного» текста MUST не
|
||||
применяться: она делает ответ функцией порядка записи, а не состояния записи.
|
||||
|
||||
Перечень значений поля `status` MUST совпадать с перечнем рубежей конвейера:
|
||||
`uploaded`, `normalized`, `submitted`, `transcribed`, `done`. Прежних значений
|
||||
`created`, `converted`, `transcribe`, `failed` и `dead` в ответе MUST не быть.
|
||||
Это объявленная ломка публичного контракта: рубеж называет достигнутое, а отказ
|
||||
перестал быть состоянием.
|
||||
|
||||
Остановленная запись MUST отдавать рубеж, на котором она остановлена, и MUST
|
||||
нести признак остановки отдельным полем `halted` со значением истины. Машинный
|
||||
текст отказа MUST в ответ не попадать: он принадлежит журналу владельца сервиса,
|
||||
а не отправителю. Отправитель узнаёт о неудаче ответом там, откуда пришла
|
||||
запись, — это нормирует capability `pipeline`.
|
||||
|
||||
Отказ без сессии MUST не зависеть от того, есть такая запись или нет: иначе по
|
||||
кодам ответа перебирается список заведённых записей.
|
||||
|
||||
Запись, принадлежащая другому, MUST отвечать тем же, чем отвечает неизвестный
|
||||
идентификатор, — кодом `404` и тем же телом. То же MUST относиться к записи без
|
||||
владельца: запись, принятая ботом, по этому адресу не достаётся никому.
|
||||
|
||||
#### Scenario: Запись найдена
|
||||
|
||||
- **GIVEN** отправитель предъявил сессию
|
||||
- **WHEN** он спрашивает рубеж своей записи
|
||||
- **THEN** ответ имеет код `200` и несёт `job_id`, `status` и `created_at`
|
||||
- **AND** значение `status` принадлежит перечню рубежей конвейера
|
||||
|
||||
#### Scenario: Запись остановлена
|
||||
|
||||
- **GIVEN** запись остановлена признаком на рубеже приведения
|
||||
- **WHEN** владелец спрашивает её рубеж
|
||||
- **THEN** поле `status` несёт рубеж приведения
|
||||
- **AND** поле `halted` несёт истину
|
||||
- **AND** машинного текста отказа в ответе нет
|
||||
|
||||
#### Scenario: Сессии нет
|
||||
|
||||
- **WHEN** программа спрашивает рубеж заведённой записи без сессии
|
||||
- **THEN** ответ имеет код `401`
|
||||
- **AND** тело ответа не несёт ни рубежа записи, ни текста расшифровки
|
||||
|
||||
#### Scenario: Без сессии неизвестная запись неотличима от заведённой
|
||||
|
||||
- **WHEN** программа без сессии спрашивает рубеж заведённой записи, а затем
|
||||
рубеж по неизвестному идентификатору
|
||||
- **THEN** оба ответа имеют код `401`
|
||||
|
||||
#### Scenario: Чужая запись неотличима от неизвестной
|
||||
|
||||
- **GIVEN** запись заведена одним вошедшим
|
||||
- **WHEN** её рубеж спрашивает другой вошедший
|
||||
- **THEN** ответ имеет код `404` и то же тело, что и ответ по неизвестному
|
||||
идентификатору
|
||||
- **AND** тело ответа не несёт ни рубежа записи, ни текста расшифровки
|
||||
|
||||
#### Scenario: Расшифровки ещё нет
|
||||
|
||||
- **GIVEN** отправитель предъявил сессию
|
||||
- **WHEN** он спрашивает рубеж своей записи, которая ещё не дошла до текста
|
||||
- **THEN** поля `transcription_text` в ответе нет вовсе
|
||||
|
||||
#### Scenario: Записи с таким идентификатором нет
|
||||
|
||||
- **GIVEN** отправитель предъявил сессию
|
||||
- **WHEN** программа спрашивает рубеж по неизвестному идентификатору
|
||||
- **THEN** ответ имеет код `404` и сообщение о ненайденной записи
|
||||
Reference in New Issue
Block a user