package contract import ( "io" "git.vakhrushev.me/av/transcriber/internal/entity" ) // WorkFile — рабочая копия файла на диске: её просят шаги, отдающие файл // внешней программе, потому что `ffmpeg` и `ffprobe` принимают имя аргументом. // // Заводится копия одним способом — репозиторием файлов, — и убирает её за собой // Close. Каждый шаг, заводящий копию сам, повторял бы и обязанность прибрать, а // забытая копия это шестичасовая запись во временном каталоге, о которой не // узнает никто. type WorkFile interface { // Path — имя копии на диске, годное для внешней программы. Path() string // Size — длина копии в байтах на момент вызова. Size() (int64, error) // Close убирает копию. Зовётся на любом исходе, включая отказ. Close() error } // FileMeta — что известно о копии сверх её содержимого. type FileMeta struct { // Format — расширение без точки, в нижнем регистре. Format string // DurationMs — длительность, если её удалось прочитать. DurationMs int64 } type FileRepository interface { // Stage принимает содержимое потоком в рабочую копию с заданным // расширением: по нему внешняя программа выбирает разбор. В память запись // целиком не читается — расчётный потолок шесть часов. Stage(ext string, content io.Reader) (WorkFile, error) // StageEmpty заводит пустую рабочую копию с заданным расширением — под // результат внешней программы, которая пишет по имени. StageEmpty(ext string) (WorkFile, error) // Localize выдаёт рабочую копию хранимого файла. Localize(fileID string) (WorkFile, error) // Create кладёт рабочую копию в хранилище под именем name и заводит запись о // файле. Имя задаёт сервис: умолчание хранилища, строящее его из имени // отправителя, не применяется. // // ownerID — владелец записи, которой файл принадлежит, и он обязателен: // колонка владельца пустого значения не принимает, пустой отвергается // схемой. Владелец лежит своей колонкой, а не выводится через запись: файл // переживает свою запись — шаг заводит его до сохранения, и потерянный // захват оставляет файл с владельцем и без ссылки. Create(name string, work WorkFile, meta FileMeta, ownerID string) (*entity.File, error) GetByID(id string) (*entity.File, error) // Open отдаёт содержимое хранимого файла потоком. Open(fileID string) (io.ReadCloser, error) } // AcquiredRecord — то, что отдаёт захват: идентификатор записи и признак // **этого** захвата. // // Перечня колонок здесь нет намеренно. Захват, возвращавший колонки поимённо, // требовал править их в четырёх местах сразу, и забытая колонка приезжала // нулевой, а первое же сохранение писало этот ноль поверх значения. Колонки шаг // читает обычным чтением. type AcquiredRecord struct { ID string // Holder — значение, уникальное для каждого захвата. Запись результата // условна по нему, а не по занятости записи: захват, перевыданный другому по // протуханию срока или после снятия остановки человеком, обязан обратить // запись первого в отказ. Holder string } // RecordCursor — положение в ленте записей, заданное **полным** ключом // сортировки. Одного времени мало: у записей, принятых одним запросом, оно // совпадает, и порядок между ними иначе не определён. type RecordCursor struct { CreatedAt string ID string } // RecordQuery — что спрашивают у ленты записей. type RecordQuery struct { // OwnerID обязателен: пустой не совпадает ни с одной записью. OwnerID string // Filter — состояние записи. Пустой значит «все». Filter *entity.ListFilter // Cursor — положение, с которого продолжать. Пустой значит «сначала». Cursor *RecordCursor Limit int } // RecordPage — страница ленты. Ключ следующей страницы пуст, когда страница // последняя. type RecordPage struct { Items []*entity.AudioRecord NextCursor *RecordCursor TotalItems int } type AudioRecordRepository interface { Create(record *entity.AudioRecord) error // List отдаёт страницу записей владельца, новыми сверху, не читая ни // расшифровки, ни структуры реплик. List(q RecordQuery) (*RecordPage, error) // ResolveTopicNames разрешает темы названиями одним запросом на страницу и // сужает их владельцем: словарь тем свой у каждого человека. ResolveTopicNames(ownerID string, ids []string) (map[string]string, error) // Save сохраняет запись, захват которой держит holder. Захват, доставшийся // за время работы другому, даёт LostAcquisitionError и запись не проводит. // Пустой holder снимает эту условность и в конвейере не употребляется: все // его шаги получают признак захвата от FindAndAcquire. Save(record *entity.AudioRecord, holder string) error // GetByID отдаёт запись, только если её владелец — ownerID. Чужая запись, // ничья и несуществующая дают одну и ту же ошибку: по разнице ответов иначе // перебирается список заведённых записей. // // Владелец здесь обязателен, и пустой ownerID не совпадает ни с чем — // включая записи без владельца. Правило записано со стороны спрашивающего: // обязательность, которую держит одна лишь подпись метода, пустую строку // пропускает. GetByID(id, ownerID string) (*entity.AudioRecord, error) // Get отдаёт запись без сужения владельцем: им пользуется конвейер, чья // выборка владельцем не сужается. Get(id string) (*entity.AudioRecord, error) // FindAndAcquire забирает пригодную к работе запись одним неделимым шагом и // увеличивает число её отказов. Отбор идёт по рабочим рубежам, паузе, сроку // протухания захвата и отсутствию признака остановки; срок протухания // приезжает с рубежом и пишется в саму запись. // // Работы нет — JobNotFoundError. FindAndAcquire(stages []entity.Stage) (*AcquiredRecord, error) } // TextRepository — тексты записи. Пара «запись и вид» уникальна: повтор // прерванного шага не заводит второй строки. type TextRepository interface { Put(recordID, kind, contents string) (*entity.Text, error) GetByID(id string) (*entity.Text, error) } // StructureRepository — структура реплик записи. Пара «запись и версия разбора» // уникальна по той же причине. type StructureRepository interface { Put(recordID string, version int, replicas []entity.Replica) (*entity.Structure, error) GetByID(id string) (*entity.Structure, error) } // RecognitionRepository — попытка распознавания у внешнего провайдера. type RecognitionRepository interface { // Create заводит строку попытки **до** обращения к провайдеру: окно между // его ответом и записью идентификатора — то место, где теряется оплаченное. Create(recognition *entity.Recognition) error // Submitted сохраняет адрес аудио и идентификатор заведённой операции. По // последнему повторный шаг узнаёт, что за эту запись уже заплачено. Submitted(id, sourceURI, externalID string) error // Finish отмечает завершение операции и кладёт сырой ответ вложением. Finish(id string, raw []byte) error GetByID(id string) (*entity.Recognition, error) // ReadRaw отдаёт сохранённый ответ провайдера. Зовётся только тогда, когда // ответ нужен: шаг опроса читает строку попытки без него. ReadRaw(id string) ([]byte, error) } // RecordEventRepository — журнал событий записи. type RecordEventRepository interface { Append(event *entity.RecordEvent) error }