# recognition Specification ## Purpose TBD - created by archiving change record-centric-model. Update Purpose after archive. ## Requirements ### Requirement: Попытка распознавания хранится отдельно от записи Сервис SHALL держать всё, что принадлежит внешнему распознавателю, отдельной строкой, связанной с аудиозаписью, и MUST не хранить это колонками самой записи. К попытке относятся имя провайдера, имя модели, идентификатор операции у провайдера, адрес, по которому провайдер читал аудио, время начала и время завершения. Разрез проходит по одной границе: **зависит ли вещь от провайдера распознавания**. Идентификатор операции — самое провайдерское, что есть в модели, а копия аудио во внешнем хранилище существует только потому, что сегодняшний провайдер читает запись по адресу; другой провайдер её не потребует. Оставленные колонками записи, они делают смену провайдера правкой доменной сущности. Копия аудио во внешнем хранилище MUST не считаться файлом записи: у записи остаётся ровно две своих копии — принятая и приведённая, — а ключ объекта живёт в строке попытки. #### Scenario: Идентификатор операции лежит в попытке - **GIVEN** запись отправлена на распознавание - **WHEN** смотрят, где лежит идентификатор операции у провайдера - **THEN** он лежит в строке попытки распознавания - **AND** колонки с ним у самой записи нет #### Scenario: Копия во внешнем хранилище не подменяет файл записи - **GIVEN** запись прошла отправку на распознавание - **WHEN** смотрят ссылки записи на файлы - **THEN** они ведут на принятую и на приведённую копии - **AND** ключ объекта во внешнем хранилище лежит в строке попытки ### Requirement: Сырой ответ провайдера сохраняется целиком Сервис SHALL сохранять ответ распознавателя целиком, в том виде, в каком он пришёл, и MUST хранить его вложением, а не колонкой строки попытки. Хранится он потому, что **результат операции у провайдера не переспрашивается**: связь реплики с говорящим сервис строить пока не умеет, и когда научится, архив пересчитается из сохранённого без повторной оплаты. Вложением, а не колонкой, — потому что шаг опроса читает строку попытки часто, а хранилище читает запись целиком: ответ на многочасовую запись, положенный колонкой, ехал бы в память при каждом опросе. Чтение строки попытки шагом опроса MUST не тянуть за собой сохранённый ответ. Сохранённый ответ — это полный текст речи, и закрыт он MUST быть наравне с самой записью: поле вложения помечено защищённым, правило просмотра пускает только владельца связанной записи, ссылка не попадает ни в журнал, ни в метку метрики. Норму держит capability `storage`, требование «Содержимое записи закрыто во всех коллекциях, где лежит»; здесь она названа потому, что коллекция попыток — то место, куда содержимое приезжает впервые. #### Scenario: Ответ сохранён и читается позже - **GIVEN** распознавание завершилось и ответ провайдера получен - **WHEN** запись доходит до конечного рубежа - **THEN** сохранённый ответ доступен по строке попытки целиком #### Scenario: Опрос не тянет сохранённый ответ - **GIVEN** у попытки распознавания есть сохранённый ответ - **WHEN** шаг опроса читает строку попытки - **THEN** сохранённый ответ в память при этом не читается ### Requirement: Структура реплик строится из сохранённого ответа Сервис SHALL строить структуру реплик записи из сохранённого ответа провайдера и MUST не обращаться к провайдеру повторно ради неё. Структура MUST хранить время каждой реплики и MUST лежать отдельной строкой со ссылкой с записи, а не колонкой записи. У структуры MUST быть номер версии её вида: разбор сохранённого ответа изменится раньше, чем архив пересчитают, и по номеру видно, какой разбор её построил. Говорящих структура сегодня не размечает: связь реплики с разбором говорящего у провайдера не выяснена. Требование этого и не заказывает — оно заказывает источник, из которого разметка станет возможной без повторной оплаты. #### Scenario: Структура собрана без обращения к провайдеру - **GIVEN** ответ провайдера сохранён - **WHEN** сервис строит структуру реплик - **THEN** структура собрана с временем каждой реплики - **AND** к провайдеру не уходит ни одного обращения ### Requirement: Разбор формата провайдера не выходит за адаптер Распознаватель SHALL отдавать сервису доменный результат — реплики со временем, плоский текст и байты ответа на хранение, — и MUST не отдавать сырой формат провайдера. Ни один шаг конвейера MUST не знать, каким потоком и какими полями провайдер отвечает. Сегодня разбор потока лежит в шаге: адаптер отдаёт строку, склеенную из альтернатив, и всё, что провайдер сказал сверх текста, теряется на границе контракта. #### Scenario: Шаг получает реплики, а не поток провайдера - **WHEN** шаг конвейера забирает результат распознавания - **THEN** он получает реплики со временем, плоский текст и байты на хранение - **AND** формата провайдера в этом результате нет ### Requirement: Заливка и отправка на распознавание разделены Сервис SHALL разделять укладку аудио туда, откуда провайдер его прочитает, и отправку операции распознавания: это два разных обращения с разной ценой повтора. Повтор укладки MUST быть бесплатен и класть объект под тем же ключом; повтор отправки оплачивается наружу и MUST не происходить, когда операция уже заведена. Разделение нужно затем, чтобы шаг мог проверить сделанное прежде, чем платить: объект нужного размера на месте — укладку MUST не повторять; идентификатор операции в строке попытки есть — отправку MUST не повторять. Строка попытки MUST заводиться **до** обращения к провайдеру: окно между ответом провайдера и записью идентификатора — то место, где теряется оплаченное. Мягкую остановку сервиса отправка MUST переживать своим пределом по времени; полной защиты от жёсткого обрыва процесса требование не даёт и дать не может — это остаточный риск, названный в дизайне, а не норма. #### Scenario: Объект уже лежит, а операции ещё нет - **GIVEN** аудио уже уложено туда, откуда провайдер его читает, и размер совпадает - **AND** идентификатора операции в строке попытки нет - **WHEN** шаг повторяется - **THEN** укладка не повторяется - **AND** операция отправляется #### Scenario: Операция уже заведена - **GIVEN** в строке попытки есть идентификатор операции - **WHEN** шаг повторяется - **THEN** отправка не повторяется - **AND** шаг переходит к опросу этой операции #### Scenario: Операция принята, а сервис мягко останавливают - **GIVEN** отправка операции ушла провайдеру - **AND** сервис в эту минуту останавливают мягко - **WHEN** провайдер отвечает идентификатором операции - **THEN** идентификатор сохраняется в строке попытки - **AND** повторная отправка той же записи не заводится