внутренняя модель перестроена вокруг аудиозаписи
- audiorecords вместо transcribe_jobs: приложения (texts, structures, recognitions, record_events, topics) живут своими коллекциями, ссылки на исходник и на приведённую копию перестали переставляться - рубеж называет достигнутое, отказ стал признаком остановки с причиной, а сторожей стало двое: число отказов и время в рубеже - воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
This commit is contained in:
+219
-59
@@ -2,14 +2,17 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
Где живут запись, её метаданные и её файл: раскладка каталога данных, приведение
|
||||
схемы при подъёме, отдача файла ссылкой по токену, собственная поверхность
|
||||
хранилища и панель владельца.
|
||||
Где живут аудиозапись, её приложения и её файлы: раскладка каталога данных,
|
||||
приведение схемы при подъёме, отдача файла ссылкой по токену, собственная
|
||||
поверхность хранилища и панель владельца.
|
||||
|
||||
Приём и опрос готовности нормирует `intake`, вход и сессию — `access`.
|
||||
Сознательно не описаны: перенос прежних данных — его нет по решению задачи
|
||||
`pocketbase-storage`; удаление записей и файлов — сервис объявлен архивом
|
||||
2026-08-11, а удаление приносит задача `delete-record`.
|
||||
Приём и опрос готовности нормирует `intake`, вход и сессию — `access`, попытку
|
||||
распознавания у внешнего провайдера — `recognition`.
|
||||
|
||||
Сознательно не описаны: перенос прежних данных — его нет ни по решению задачи
|
||||
`pocketbase-storage`, ни по решению владельца 2026-08-14, которым прежние записи
|
||||
удалены вместе с остановкой сервиса; удаление записей и файлов — сервис объявлен
|
||||
архивом 2026-08-11, а удаление приносит задача `delete-record`.
|
||||
## Requirements
|
||||
### Requirement: Сервис поднимается на чистом каталоге данных
|
||||
|
||||
@@ -208,22 +211,24 @@ MUST завести свою схему и принимать записи об
|
||||
|
||||
### Requirement: Владелец видит записи в панели
|
||||
|
||||
Сервис SHALL давать владельцу панель, где задача видна строкой, отбирается по
|
||||
своему идентификатору и правится, а её файл слушается и скачивается.
|
||||
Сервис SHALL давать владельцу панель, где аудиозапись видна строкой, отбирается
|
||||
по своему идентификатору и правится, а её файлы слушаются и скачиваются.
|
||||
|
||||
Панель MUST отдаваться тем же сервисом по своему адресу и MUST не требовать
|
||||
второго процесса.
|
||||
|
||||
Панель — вход в задачу наравне с конвейером, а не окно просмотра, и правка
|
||||
состояния задачи в ней MUST подчиняться тем же правилам перехода, что и правка
|
||||
из кода: служебные поля прошлого состояния — признак захвата, время захвата,
|
||||
пауза, число попыток — MUST очищаться. Иначе владелец, вернувший мёртвую задачу в
|
||||
работу, получит задачу, которая не выдаётся захвату до конца прежнего срока и
|
||||
умирает от первого же отказа, — и не узнает об этом.
|
||||
Панель — вход в запись наравне с конвейером, а не окно просмотра. Снятие
|
||||
признака остановки в панели MUST возвращать запись в работу с сохранённого
|
||||
рубежа и MUST очищать служебные поля прошлого захвата — признак захвата, срок
|
||||
его протухания, паузу, число отказов — и MUST заново ставить время входа в
|
||||
рубеж. Правка рубежа руками MUST делать то же самое. Иначе владелец, вернувший
|
||||
запись в работу, получит запись, которая не выдаётся захвату до конца прежнего
|
||||
срока, останавливается от первого же отказа или останавливается снова первым же
|
||||
захватом по пределу времени, — и не узнает об этом.
|
||||
|
||||
Задача, заведённая в панели руками, MUST не уносить сервис: поля, без которых
|
||||
Запись, заведённая в панели руками, MUST не уносить сервис: поля, без которых
|
||||
шаг конвейера не может работать, MUST быть обязательными в самой схеме, а
|
||||
перечень состояний — закрытым.
|
||||
перечень рубежей — закрытым.
|
||||
|
||||
Панель разграничению доступа сервиса не подчиняется: вошедший в неё видит все
|
||||
записи, все файлы и всех пользователей разом. Закрывает её контур выкладки, а не
|
||||
@@ -231,18 +236,20 @@ MUST завести свою схему и принимать записи об
|
||||
|
||||
#### Scenario: Принятая запись видна владельцу
|
||||
|
||||
- **GIVEN** запись принята и её задача заведена
|
||||
- **WHEN** владелец отбирает задачи по идентификатору принятой
|
||||
- **THEN** он видит её строкой со своим состоянием
|
||||
- **AND** файл этой записи скачивается из той же строки
|
||||
- **GIVEN** запись принята и заведена
|
||||
- **WHEN** владелец отбирает записи по идентификатору принятой
|
||||
- **THEN** он видит её строкой со своим рубежом
|
||||
- **AND** её файл скачивается из той же строки
|
||||
|
||||
#### Scenario: Мёртвую задачу вернули в работу правкой в панели
|
||||
#### Scenario: Остановленную запись вернули в работу правкой в панели
|
||||
|
||||
- **GIVEN** задача в состоянии «мертва» с исчерпанными попытками и признаком
|
||||
- **GIVEN** запись остановлена признаком, с накопленными отказами и признаком
|
||||
прежнего захвата
|
||||
- **WHEN** владелец меняет её состояние на рабочее
|
||||
- **THEN** признак захвата, время захвата, пауза и число попыток очищены
|
||||
- **AND** ближайший захват выдаёт задачу
|
||||
- **AND** остановленной она простояла дольше предела времени в рубеже
|
||||
- **WHEN** владелец снимает признак остановки
|
||||
- **THEN** признак захвата, срок его протухания, пауза и число отказов очищены
|
||||
- **AND** время входа в рубеж поставлено заново
|
||||
- **AND** ближайший захват выдаёт запись с сохранённого рубежа
|
||||
|
||||
### Requirement: Пароль владельца от панели не лежит в конфигурации
|
||||
|
||||
@@ -278,8 +285,8 @@ MUST завести свою схему и принимать записи об
|
||||
|
||||
### Requirement: Владелец задачи лежит связью с учётной записью
|
||||
|
||||
Хранилище SHALL держать владельца задачи расшифровки отдельной колонкой — связью
|
||||
с учётной записью, — и эта колонка MUST не иметь умолчания: запись, чей владелец
|
||||
Хранилище SHALL держать владельца аудиозаписи отдельной колонкой — связью с
|
||||
учётной записью, — и эта колонка MUST не иметь умолчания: запись, чей владелец
|
||||
не назван, не достаётся никому по недосмотру схемы.
|
||||
|
||||
Колонка MUST допускать пустое значение, и это решение с названной ценой: записи,
|
||||
@@ -287,35 +294,33 @@ MUST завести свою схему и принимать записи об
|
||||
записью сервис не ведёт. Обязательность для приёма по HTTP держит сама
|
||||
capability `intake`, а не схема.
|
||||
|
||||
Колонка приезжает **новым шагом схемы**: применённый шаг не переписывается.
|
||||
Записей, заведённых до этого шага, сервис не переносит — проект заводится с
|
||||
чистого листа.
|
||||
Владелец MUST не назначаться и не меняться конвейером.
|
||||
|
||||
#### Scenario: Колонка появляется на пустой базе
|
||||
|
||||
- **WHEN** сервис поднимается на чистом каталоге данных
|
||||
- **THEN** у таблицы задач есть колонка владельца
|
||||
- **THEN** у аудиозаписи есть колонка владельца
|
||||
- **AND** умолчания у неё нет
|
||||
|
||||
#### Scenario: Конвейер владельца не назначает
|
||||
|
||||
- **GIVEN** запись с владельцем прошла шаг конвейера
|
||||
- **WHEN** смотрят её владельца
|
||||
- **THEN** он прежний
|
||||
|
||||
### Requirement: Файл записи сужается владельцем наравне с задачей
|
||||
|
||||
Хранилище SHALL держать владельца и у файла записи — той же связью с учётной
|
||||
записью, тем же шагом схемы, — и правило просмотра файлов MUST пускать к файлу
|
||||
только его владельца. Прежнее правило пускало всякого узнанного, и знание
|
||||
идентификатора файловой записи равнялось праву скачать чужое аудио.
|
||||
записью, — и правило просмотра файлов MUST пускать к файлу только его владельца.
|
||||
|
||||
Без этого требования разграничение закрывает метаданные задачи и оставляет
|
||||
открытым содержимое — то самое, что оно и заведено прятать. Хуже самой дыры была
|
||||
бы отметка о закрытии: паспорт и модель угроз называют исполнителем этой работы
|
||||
именно эту задачу, и слово «закрыто» скрыло бы открытый путь.
|
||||
|
||||
Владелец файла MUST назначаться там же, где владелец задачи, — при приёме, из
|
||||
Владелец файла MUST назначаться там же, где владелец записи, — при приёме, из
|
||||
предъявленной сессии, — и MUST оставаться пустым у файлов, заведённых конвейером
|
||||
для записи без владельца.
|
||||
|
||||
Ссылка на файл в задаче переставляется каждым шагом конвейера, поэтому владелец
|
||||
файла MUST лежать своей колонкой, а не выводиться через задачу: исходная копия
|
||||
после конвертации не связана с задачей ничем.
|
||||
Ссылки на файлы у записи две — на принятую копию и на приведённую, — и обе живут
|
||||
до конца, но владелец файла MUST по-прежнему лежать своей колонкой, а не
|
||||
выводиться через запись: файл переживает свою запись, и заведённый шагом до
|
||||
сохранения записи он остаётся с владельцем и без ссылки.
|
||||
|
||||
Отказ наступает **на переходе по ссылке**, а не на выдаче токена файла: токен
|
||||
хранилище выдаёт на предъявителя, а не на файл, и о файле при выдаче не
|
||||
@@ -343,15 +348,21 @@ capability `intake`, а не схема.
|
||||
|
||||
### Requirement: Учётная запись с записями не удаляется
|
||||
|
||||
Хранилище SHALL отвергать удаление учётной записи, у которой остались задачи
|
||||
расшифровки **либо файлы**. Отказ MUST называть причину, и MUST доезжать до
|
||||
Хранилище SHALL отвергать удаление учётной записи, у которой остались
|
||||
аудиозаписи **либо файлы**. Отказ MUST называть причину, и MUST доезжать до
|
||||
спрашивающего: хранилище пропускает наружу только свою ошибку роутера, а всякую
|
||||
другую подменяет сообщением про обязательную связь — подсказкой, по которой
|
||||
владелец панели пойдёт удалять записи руками.
|
||||
|
||||
Считаются обе коллекции с владельцем. Файл переживает свою задачу: шаг конвейера
|
||||
заводит его до сохранения задачи, и потерянный захват оставляет файл с владельцем
|
||||
и без ссылки.
|
||||
Считаются **все** коллекции с колонкой владельца, и перечень их MUST жить одним
|
||||
местом: коллекция, пропущенная в счёте, пропускает удаление вперёд, и наружу
|
||||
приезжает не наш отказ с причиной, а подсказка библиотеки про обязательную связь
|
||||
— та самая, по которой владелец панели пойдёт удалять записи руками. Сегодня их
|
||||
три: аудиозаписи, файлы и словарь тем.
|
||||
|
||||
Файл переживает свою запись: шаг конвейера заводит его до сохранения записи, и
|
||||
потерянный захват оставляет файл с владельцем и без ссылки. Тема переживает её
|
||||
так же: словарь принадлежит человеку, а не записи.
|
||||
|
||||
Запрет MUST ставить сама сборка хранилища, а не вызывающий: сборка, забывшая его
|
||||
позвать, теряет защиту молча — и теряла, пока запрет вешался отдельной строкой
|
||||
@@ -361,12 +372,6 @@ capability `intake`, а не схема.
|
||||
удалить **свою** учётную запись запросом, так что запрет закрывает и публичную
|
||||
поверхность.
|
||||
|
||||
Требование заведено вместо прежнего «удаление не уносит задачи следом»: оно
|
||||
выглядело выполненным, а на деле хранилище при выключенном каскаде **снимает
|
||||
ссылку** — задачи остаются, но становятся ничьими, а ничья задача не достаётся
|
||||
по API никому. Архив человека исчезал бы молча и восстановлению не подлежал:
|
||||
прежнего владельца не остаётся нигде.
|
||||
|
||||
Цена требования названа прямо: владелец панели упирается в отказ, а способа
|
||||
удалить записи в сервисе пока нет вовсе — его приносит задача про удаление
|
||||
записи. До неё удаление учётной записи с записями невозможно, и это осознанный
|
||||
@@ -374,20 +379,175 @@ capability `intake`, а не схема.
|
||||
|
||||
#### Scenario: Удаление учётной записи с записями отвергается
|
||||
|
||||
- **GIVEN** у учётной записи есть задачи расшифровки
|
||||
- **GIVEN** у учётной записи есть аудиозаписи
|
||||
- **WHEN** её удаляют
|
||||
- **THEN** удаление не проходит, а отказ называет причину
|
||||
- **AND** задачи и их владелец остаются прежними
|
||||
- **AND** записи и их владелец остаются прежними
|
||||
|
||||
#### Scenario: Учётная запись с одними файлами тоже не удаляется
|
||||
|
||||
- **GIVEN** у учётной записи остались файлы, но задач нет
|
||||
- **GIVEN** у учётной записи остались файлы, но записей нет
|
||||
- **WHEN** её удаляют
|
||||
- **THEN** удаление не проходит, а владелец файлов остаётся прежним
|
||||
|
||||
#### Scenario: Учётная запись с одними темами тоже не удаляется
|
||||
|
||||
- **GIVEN** у учётной записи остались темы словаря, но ни записей, ни файлов нет
|
||||
- **WHEN** её удаляют
|
||||
- **THEN** удаление не проходит, а отказ называет причину нашими словами
|
||||
|
||||
#### Scenario: Учётная запись без записей удаляется
|
||||
|
||||
- **GIVEN** у учётной записи нет ни задач, ни файлов
|
||||
- **GIVEN** у учётной записи нет ни аудиозаписей, ни файлов, ни тем
|
||||
- **WHEN** её удаляют
|
||||
- **THEN** удаление проходит
|
||||
|
||||
### Requirement: Аудиозапись — центральная сущность хранилища
|
||||
|
||||
Хранилище SHALL держать аудиозапись отдельной сущностью, а всё, что к ней
|
||||
приложено, — отдельными строками со ссылками с записи. Приложениями считаются
|
||||
файлы, тексты, структура реплик, темы, журнал событий и попытка распознавания.
|
||||
|
||||
Поля, которыми распоряжается очередь — признак захвата, срок его протухания,
|
||||
пауза, число отказов, время входа в рубеж, — MUST не соседствовать с содержимым
|
||||
записи в одной строке настолько, чтобы чтение очереди тянуло содержимое: сегодня
|
||||
расшифровка лежит колонкой той же строки и читается при каждом захвате.
|
||||
|
||||
Запись MUST нести заголовок и краткое описание своими колонками: они читаются
|
||||
вместе со списком, сотней штук разом. Расшифровка и вычитанный текст MUST лежать
|
||||
отдельными строками: они читаются по открытию одной записи.
|
||||
|
||||
#### Scenario: Список читается без содержимого
|
||||
|
||||
- **GIVEN** у записи есть расшифровка
|
||||
- **WHEN** читают запись ради её рубежа и заголовка
|
||||
- **THEN** текст расшифровки при этом не читается
|
||||
|
||||
### Requirement: Содержимое записи закрыто во всех коллекциях, где лежит
|
||||
|
||||
Всякая коллекция, куда переезжает содержимое аудиозаписи, SHALL быть закрыта
|
||||
наравне с самой записью: её правило просмотра MUST не открывать содержимое
|
||||
никому, кроме владельца связанной записи, а поле, хранящее файл или вложение,
|
||||
MUST быть помечено защищённым.
|
||||
|
||||
Пока содержимое отдаётся собственным адресом сервиса, а не поверхностью
|
||||
хранилища, правило просмотра MUST оставаться незаданным — то есть «только
|
||||
владелец панели». Непустое правило открывает перечисление коллекции, и заводить
|
||||
его раньше, чем появится потребитель, значит открывать поверхность впрок:
|
||||
норму держит требование «Наружу хранилище отдаёт только то, что заказано».
|
||||
|
||||
Требование распространяется на все коллекции приложений — тексты, структуру
|
||||
реплик, попытку распознавания с её сохранённым ответом, журнал событий и темы, —
|
||||
и заводится потому, что содержимое **переезжает** из одной строки в шесть. Норма
|
||||
о защищённом поле файла сегодня написана про файл записи, а сырой ответ
|
||||
распознавателя — это полный текст речи в другой коллекции: реализация, следующая
|
||||
только прежней норме, завела бы поле с умолчанием библиотеки, и ссылка на него
|
||||
отдавала бы расшифровку любому, кто её знает, без сессии.
|
||||
|
||||
Ссылка на такое вложение MUST не попадать ни в журнал, ни в метку метрики, ни в
|
||||
ответ отправителю — теми же словами, какими это нормировано для файла записи.
|
||||
|
||||
Умолчание библиотеки здесь не годится ни в одном месте: незаданное правило
|
||||
просмотра значит «только владелец панели» и отнимает содержимое у самого
|
||||
владельца записи, а незащищённое поле файла отдаёт его всем.
|
||||
|
||||
#### Scenario: Чужой сохранённый ответ не отдаётся
|
||||
|
||||
- **GIVEN** запись принята одним вошедшим и прошла распознавание
|
||||
- **WHEN** другой вошедший идёт по ссылке на сохранённый ответ провайдера
|
||||
- **THEN** содержимого он не получает
|
||||
|
||||
#### Scenario: Без сессии содержимое не отдаётся
|
||||
|
||||
- **WHEN** ссылку на сохранённый ответ провайдера запрашивают без сессии
|
||||
- **THEN** приходит отказ, а содержимого в ответе нет
|
||||
|
||||
#### Scenario: Перечисление приложений закрыто
|
||||
|
||||
- **WHEN** запрос без прав владельца просит список записей коллекции текстов
|
||||
- **THEN** приходит отказ
|
||||
|
||||
### Requirement: Ссылки на исходник и приведённую копию живут порознь
|
||||
|
||||
Аудиозапись SHALL нести две отдельные ссылки на файлы — на принятую копию и на
|
||||
копию, приведённую к рабочему формату, — и шаг конвейера MUST не переставлять
|
||||
одну ссылку на свой результат.
|
||||
|
||||
Сегодня ссылка одна, и её переставляет каждый шаг: у прошедшей конвейер записи
|
||||
она ведёт на копию во внешнем хранилище, а принятого человеком файла не найти
|
||||
ничем. Послушать загруженное нечем именно поэтому.
|
||||
|
||||
Обе копии MUST оставаться доступными после того, как запись прошла конвейер.
|
||||
|
||||
#### Scenario: После конвейера доступны обе копии
|
||||
|
||||
- **GIVEN** запись прошла конвейер целиком
|
||||
- **WHEN** смотрят её ссылки на файлы
|
||||
- **THEN** ссылка на принятую копию и ссылка на приведённую заполнены
|
||||
- **AND** обе открываются
|
||||
|
||||
### Requirement: Тексты и структура лежат отдельно от записи
|
||||
|
||||
Хранилище SHALL держать тексты записи отдельными строками, каждая со своим видом
|
||||
текста, и структуру реплик — своей строкой. Запись MUST ссылаться на них, а не
|
||||
хранить их колонками.
|
||||
|
||||
Видов текста больше одного: сырая расшифровка и вычитанный текст. Колонкой на
|
||||
каждый вид схема росла бы с каждым новым видом, а необратимый шаг схемы платится
|
||||
за каждую такую колонку отдельно.
|
||||
|
||||
**Приложение MUST быть уникально по паре «запись и вид»**, а структура — по паре
|
||||
«запись и версия разбора». Шаг завершения пишет текст, структуру и сохранённый
|
||||
ответ несколькими операциями и только потом двигает рубеж: прерванный на середине
|
||||
и повторённый с прежнего рубежа, он завёл бы второй комплект строк, и вопрос
|
||||
«какой текст отдавать человеку» стал бы вопросом порядка записи, а не состояния.
|
||||
|
||||
Потребитель текста MUST называть **вид**, который берёт, а не брать последний
|
||||
записанный: иначе исход зависит от порядка записи. Ответ опроса готовности берёт
|
||||
сырую расшифровку — норму держит capability `intake`.
|
||||
|
||||
#### Scenario: Расшифровка лежит своей строкой
|
||||
|
||||
- **GIVEN** запись прошла распознавание
|
||||
- **WHEN** смотрят, где лежит текст расшифровки
|
||||
- **THEN** он лежит отдельной строкой, на которую запись ссылается
|
||||
|
||||
#### Scenario: Повтор шага не заводит второй расшифровки
|
||||
|
||||
- **GIVEN** шаг завершения записал расшифровку и оборвался до смены рубежа
|
||||
- **WHEN** шаг повторяется с прежнего рубежа
|
||||
- **THEN** строка расшифровки у записи одна
|
||||
|
||||
### Requirement: Словарь тем ведётся по владельцу
|
||||
|
||||
Хранилище SHALL держать темы отдельной коллекцией, и тема MUST быть уникальна в
|
||||
паре «владелец и название»: словарь тем свой у каждого человека. У записи MUST
|
||||
быть не больше пяти тем.
|
||||
|
||||
Коллекцией, а не набором строк в записи, — потому что перечень тем человека
|
||||
нужен целиком перед каждым обращением к модели, а собрать его из наборов строк
|
||||
можно только перебором всех его записей.
|
||||
|
||||
Потолок в пять тем MUST быть у самой записи: без него часовой разговор даёт два
|
||||
десятка тем, и словарь распухает за неделю.
|
||||
|
||||
Название темы выведено из содержимого записи, а перечень тем человека — слепок
|
||||
того, о чём он вообще говорит. В журнал сервиса темы MUST не попадать наравне с
|
||||
текстом расшифровки.
|
||||
|
||||
Ни один шаг этого изменения тем не пишет и не читает: место заводится вперёд,
|
||||
чтобы задача, считающая темы языковой моделью, не платила вторым необратимым
|
||||
шагом схемы. Цена решения названа прямо — имена коллекции и её колонок
|
||||
закрепляются раньше, чем известен их потребитель.
|
||||
|
||||
#### Scenario: Тема одного человека не мешает теме другого
|
||||
|
||||
- **GIVEN** у двух владельцев заведена тема с одинаковым названием
|
||||
- **WHEN** смотрят словарь тем
|
||||
- **THEN** это две разные темы, каждая своего владельца
|
||||
|
||||
#### Scenario: Шестая тема не заводится
|
||||
|
||||
- **WHEN** записи назначают шестую тему
|
||||
- **THEN** назначение не проходит
|
||||
|
||||
|
||||
Reference in New Issue
Block a user