- убраны клиент бота, транспорт обновлений, отправитель сообщений, сборка входа при старте, секция настроек и зависимость go-telegram-bot-api; из конвейера ушла доставка ответа отправителю — исход виден опросом готовности. Колонки адресата и значение источника остались в схеме: применённые шаги не переписываются - шаг 202608140003 запрещает пустого владельца у аудиозаписи и у файла; существующие строки он не проверяет, и это принято сознательно — искать их надо запросом до выкладки - ревью нашло два пред-существующих дефекта, оба закрыты: пустой второй ответ распознавателя стирал сохранённую расшифровку, а пустая расшифровка перестала быть заметной вместе с убранной доставкой. Попутно поднят golang.org/x/image до v0.45.0 — красный шаг vulns, воспроизводился и на чистом master
136 lines
8.8 KiB
Go
136 lines
8.8 KiB
Go
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
|
|
}
|
|
|
|
type AudioRecordRepository interface {
|
|
Create(record *entity.AudioRecord) 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
|
|
}
|