Files
transcriber/openspec/specs/recognition/spec.md
T
av 1576d06735 внутренняя модель перестроена вокруг аудиозаписи
- audiorecords вместо transcribe_jobs: приложения (texts, structures,
  recognitions, record_events, topics) живут своими коллекциями, ссылки на
  исходник и на приведённую копию перестали переставляться
- рубеж называет достигнутое, отказ стал признаком остановки с причиной, а
  сторожей стало двое: число отказов и время в рубеже
- воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг
  выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
2026-08-14 20:20:33 +03:00

11 KiB

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 повторная отправка той же записи не заводится