- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
14 KiB
recognition Specification
Purpose
Что принадлежит внешнему распознавателю и как это лежит у нас: попытка распознавания отдельной строкой, сырой ответ провайдера целиком, структура реплик, построенная из сохранённого ответа, и граница, за которую разбор чужого формата не выходит.
Движение записи по рубежам нормирует pipeline, хранение текста и структуры —
storage, закрытость содержимого — storage же.
Сознательно не описан формат ответа конкретного провайдера: он живёт в адаптере и в записках разведки, а не в норме поведения.
Requirements
Requirement: Попытка распознавания хранится отдельно от записи
Сервис SHALL держать всё, что принадлежит внешнему распознавателю, отдельной строкой, связанной с аудиозаписью, и MUST не хранить это колонками самой записи. К попытке относятся имя провайдера, имя модели, идентификатор операции у провайдера, адрес, по которому провайдер читал аудио, время начала и время завершения.
Разрез проходит по одной границе: зависит ли вещь от провайдера распознавания. Идентификатор операции — самое провайдерское, что есть в модели, а копия аудио во внешнем хранилище существует только потому, что сегодняшний провайдер читает запись по адресу; другой провайдер её не потребует. Оставленные колонками записи, они делают смену провайдера правкой доменной сущности.
Копия аудио во внешнем хранилище MUST не считаться файлом записи: у записи остаётся ровно две своих копии — принятая и приведённая, — а ключ объекта живёт в строке попытки.
Scenario: Идентификатор операции лежит в попытке
- GIVEN запись отправлена на распознавание
- WHEN смотрят, где лежит идентификатор операции у провайдера
- THEN он лежит в строке попытки распознавания
- AND колонки с ним у самой записи нет
Scenario: Копия во внешнем хранилище не подменяет файл записи
- GIVEN запись прошла отправку на распознавание
- WHEN смотрят ссылки записи на файлы
- THEN они ведут на принятую и на приведённую копии
- AND ключ объекта во внешнем хранилище лежит в строке попытки
Requirement: Сырой ответ провайдера сохраняется целиком
Сервис SHALL сохранять ответ распознавателя целиком, в том виде, в каком он пришёл, и MUST хранить его отдельным файлом в каталоге данных, а не колонкой строки попытки.
Хранится он потому, что результат операции у провайдера не переспрашивается: связь реплики с говорящим сервис строить пока не умеет, и когда научится, архив пересчитается из сохранённого без повторной оплаты.
Файлом, а не колонкой, — потому что шаг опроса читает строку попытки часто, а
репозиторий читает строку целиком: ответ на многочасовую запись, положенный
колонкой, ехал бы в память при каждом опросе. Где именно этот файл лежит,
нормирует capability storage, требование «Файл записи живёт в хранилище»:
третьим файлом в подкаталоге записи, наравне с копиями аудио. Копией аудио он при
этом не считается — их у записи по-прежнему две.
Чтение строки попытки шагом опроса MUST не тянуть за собой сохранённый ответ.
Сохранённый ответ — это полный текст речи, и закрыт он MUST быть наравне с самой записью. Закрытость MUST держаться проверкой владельца в обработчике сервиса: адреса, которым сохранённый ответ читают снаружи, сервис MUST не заводить вовсе, а всякий адрес, отдающий содержимое записи, MUST судить владельца связанной аудиозаписи сам, при каждом обращении. Пометка поля защищённым и правило просмотра коллекции, которыми закрытость держалась прежде, — механизмы встроенного хранилища, и их не остаётся; норма от этого не ослабла, а перестала зависеть от настройки, которую мы не писали.
Путь к файлу сохранённого ответа MUST не попадать ни в журнал, ни в метку метрики, ни в ответ отправителю.
Норму держит capability storage, требование «Содержимое записи закрыто везде,
где лежит»; здесь она названа потому, что попытка распознавания — то место, куда
содержимое приезжает впервые.
Scenario: Ответ сохранён и читается позже
- GIVEN распознавание завершилось и ответ провайдера получен
- WHEN запись доходит до конечного рубежа
- THEN сохранённый ответ доступен по строке попытки целиком
Scenario: Опрос не тянет сохранённый ответ
- GIVEN у попытки распознавания есть сохранённый ответ
- WHEN шаг опроса читает строку попытки
- THEN сохранённый ответ в память при этом не читается
Scenario: Адреса чтения сохранённого ответа у сервиса нет
- GIVEN запись принята одним узнанным и прошла распознавание
- WHEN другой узнанный ищет адрес, которым читается сохранённый ответ этой записи
- THEN такого адреса у сервиса нет
- AND содержимого он не получает
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 повторная отправка той же записи не заводится