внутренняя модель перестроена вокруг аудиозаписи
- audiorecords вместо transcribe_jobs: приложения (texts, structures, recognitions, record_events, topics) живут своими коллекциями, ссылки на исходник и на приведённую копию перестали переставляться - рубеж называет достигнутое, отказ стал признаком остановки с причиной, а сторожей стало двое: число отказов и время в рубеже - воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/clock"
|
||||
)
|
||||
|
||||
// Рубежи конвейера. Рубеж называет **достигнутое**, а не предстоящее: по нему
|
||||
// видно, что с записью уже сделано, и потому остановленная запись продолжает с
|
||||
// места остановки, а не с начала.
|
||||
//
|
||||
// Конечный рубеж зовётся `done`: доставка ответа отправителю в конвейер не
|
||||
// входит, и слово описывает пройденный конвейер, а не полученный человеком
|
||||
// текст.
|
||||
const (
|
||||
StateUploaded = "uploaded"
|
||||
StateNormalized = "normalized"
|
||||
StateSubmitted = "submitted"
|
||||
StateTranscribed = "transcribed"
|
||||
StateDone = "done"
|
||||
)
|
||||
|
||||
// Причины остановки. Прежние состояния `failed` и `dead` схлопнуты сюда: обе
|
||||
// восстанавливаются одинаково — снятием признака, — и различие между ними
|
||||
// перестало быть структурным.
|
||||
const (
|
||||
// HaltReasonStepFailed — шаг рассудил об этой записи окончательно.
|
||||
HaltReasonStepFailed = "step_failed"
|
||||
// HaltReasonAttempts — мы повторяли и перестали.
|
||||
HaltReasonAttempts = "attempts_exhausted"
|
||||
// HaltReasonStuck — запись простояла в рубеже дольше предела.
|
||||
HaltReasonStuck = "stuck"
|
||||
)
|
||||
|
||||
const (
|
||||
SourceUnknown = "unknown"
|
||||
SourceApi = "api"
|
||||
SourceTelegram = "telegram"
|
||||
)
|
||||
|
||||
// AudioRecord — аудиозапись, центральная сущность сервиса.
|
||||
//
|
||||
// Приложения к ней — файлы, тексты, структура реплик, темы, журнал событий и
|
||||
// попытка распознавания — живут своими строками и адресуются ссылками. Поля
|
||||
// очереди соседствуют с доменом, но не с содержимым: расшифровка лежит строкой
|
||||
// `texts`, и чтение очереди её не тянет.
|
||||
type AudioRecord struct {
|
||||
Id string
|
||||
// OwnerID — учётная запись, от имени которой запись принята. Пуст у записей
|
||||
// из Telegram: связи чата с учётной записью сервис не ведёт. Назначается
|
||||
// один раз, при приёме, и конвейером не меняется.
|
||||
OwnerID *string
|
||||
Source string
|
||||
|
||||
// Title и Brief читаются вместе со списком, сотней штук разом, и потому
|
||||
// лежат колонками записи, а не строками `texts`.
|
||||
Title *string
|
||||
Brief *string
|
||||
|
||||
State string
|
||||
// StateEnteredAt ставится только сменой рубежа и возвратом записи в работу.
|
||||
// Откладывание опроса его не двигает — иначе застревание в чужой операции
|
||||
// не наступало бы никогда.
|
||||
StateEnteredAt time.Time
|
||||
|
||||
// Остановка — признак, а не рубеж: `State` при ней не стирается.
|
||||
HaltedAt *time.Time
|
||||
HaltReason *string
|
||||
ErrorText *string
|
||||
|
||||
// AcquisitionID — признак **этого** захвата, значение уникальное для каждого.
|
||||
// Запись результата условна по нему, а не по занятости записи: захват,
|
||||
// перевыданный другому — по протуханию срока или после снятия остановки
|
||||
// человеком, — обязан обратить запись первого в отказ.
|
||||
AcquisitionID *string
|
||||
AcquireExpiresAt *time.Time
|
||||
DelayTime *time.Time
|
||||
// Attempts считает **отказы** и ограничивает повторы внутри шага. Время в
|
||||
// рубеже мерит StateEnteredAt: одно число не справлялось ни с одной из двух
|
||||
// обязанностей.
|
||||
Attempts int
|
||||
|
||||
// Ссылки на файлы живут порознь и не переставляются: исходник остаётся
|
||||
// доступным после того, как запись прошла конвейер.
|
||||
OriginalFileID *string
|
||||
NormalizedFileID *string
|
||||
|
||||
StructureID *string
|
||||
TranscriptTextID *string
|
||||
LiteraryTextID *string
|
||||
RecognitionID *string
|
||||
|
||||
TgChatId *int64
|
||||
TgReplyMessageId *int
|
||||
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// AllStates — закрытый перечень рубежей для схемы хранилища.
|
||||
func AllStates() []string {
|
||||
out := make([]string, 0, len(stages))
|
||||
for _, s := range stages {
|
||||
out = append(out, s.Name)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AllHaltReasons — закрытый перечень причин остановки для схемы хранилища.
|
||||
func AllHaltReasons() []string {
|
||||
return []string{HaltReasonStepFailed, HaltReasonAttempts, HaltReasonStuck}
|
||||
}
|
||||
|
||||
// MoveToState двигает запись на новый рубеж и чистит служебные поля прошлого.
|
||||
//
|
||||
// Время входа в рубеж ставится заново: с этой минуты идёт отсчёт застревания.
|
||||
// Число отказов обнуляется — шаг, дошедший до перехода, завершился без отказа, а
|
||||
// отказы считают именно отказавшие: иначе запись, прошедшая конвейер целиком,
|
||||
// накопила бы их поштучно и остановилась бы здоровой.
|
||||
func (r *AudioRecord) MoveToState(state string) {
|
||||
now := clock.Now()
|
||||
r.State = state
|
||||
r.StateEnteredAt = now
|
||||
r.DelayTime = nil
|
||||
r.AcquisitionID = nil
|
||||
r.AcquireExpiresAt = nil
|
||||
r.Attempts = 0
|
||||
r.UpdatedAt = now
|
||||
}
|
||||
|
||||
// Postpone откладывает работу над записью: ставит паузу и снимает захват.
|
||||
//
|
||||
// Переходом это не является и потому не трогает ни рубеж, ни время входа в
|
||||
// него. Число отказов обнуляется по прежнему доводу — ожидание чужой операции
|
||||
// отказом не является.
|
||||
//
|
||||
// Прежде шаг опроса звал переход с **тем же** состоянием, и мнимость этого
|
||||
// перехода обнуляла сторожа. Без разделения время входа в рубеж сбрасывалось бы
|
||||
// на каждом опросе и повторило бы ровно тот промах, ради которого заводится.
|
||||
func (r *AudioRecord) Postpone(until time.Time) {
|
||||
r.DelayTime = &until
|
||||
r.AcquisitionID = nil
|
||||
r.AcquireExpiresAt = nil
|
||||
r.Attempts = 0
|
||||
r.UpdatedAt = clock.Now()
|
||||
}
|
||||
|
||||
// RetryAfter освобождает отказавшую запись для повтора: захват снимается, пауза
|
||||
// ставится, а число отказов сохраняется — по нему растёт пауза и наступает
|
||||
// предел.
|
||||
func (r *AudioRecord) RetryAfter(delay time.Time) {
|
||||
r.AcquisitionID = nil
|
||||
r.AcquireExpiresAt = nil
|
||||
r.DelayTime = &delay
|
||||
r.UpdatedAt = clock.Now()
|
||||
}
|
||||
|
||||
// Halt останавливает запись признаком, сохраняя достигнутый рубеж.
|
||||
//
|
||||
// Число отказов сохраняется: по нему видно, сколько раз пробовали. Захват
|
||||
// снимается — остановленная запись всё равно не выдаётся, а оставленный признак
|
||||
// захвата помешал бы первому же захвату после снятия остановки.
|
||||
func (r *AudioRecord) Halt(reason, errText string) {
|
||||
now := clock.Now()
|
||||
r.HaltedAt = &now
|
||||
r.HaltReason = &reason
|
||||
r.ErrorText = &errText
|
||||
r.AcquisitionID = nil
|
||||
r.AcquireExpiresAt = nil
|
||||
r.DelayTime = nil
|
||||
r.UpdatedAt = now
|
||||
}
|
||||
|
||||
// Resume возвращает остановленную запись в работу с сохранённого рубежа.
|
||||
//
|
||||
// Сбрасываются все три сторожа. Время входа в рубеж — тоже, и это не
|
||||
// избыточность: запись, простоявшая остановленной дольше предела, иначе
|
||||
// останавливалась бы снова первым же захватом, и перезапуск не работал бы вовсе.
|
||||
func (r *AudioRecord) Resume() {
|
||||
now := clock.Now()
|
||||
r.HaltedAt = nil
|
||||
r.HaltReason = nil
|
||||
r.ErrorText = nil
|
||||
r.Attempts = 0
|
||||
r.DelayTime = nil
|
||||
r.AcquisitionID = nil
|
||||
r.AcquireExpiresAt = nil
|
||||
r.StateEnteredAt = now
|
||||
r.UpdatedAt = now
|
||||
}
|
||||
|
||||
// IsHalted — стоит ли на записи признак остановки.
|
||||
func (r *AudioRecord) IsHalted() bool {
|
||||
return r.HaltedAt != nil
|
||||
}
|
||||
+16
-9
@@ -21,16 +21,23 @@ const (
|
||||
// дойдёт до обработчика — без строки в журнале приёма.
|
||||
const MaxRecordSize int64 = 8 << 30 // 8 ГиБ
|
||||
|
||||
// File — одна физическая копия: исходник, результат конвертации и копия во
|
||||
// внешнем хранилище — три разные записи.
|
||||
// File — одна физическая копия записи. Их ровно две: принятая и приведённая к
|
||||
// рабочему формату. Копия во внешнем хранилище файлом записи не считается — она
|
||||
// существует только потому, что провайдер читает аудио по адресу, и её ключ
|
||||
// живёт в строке попытки распознавания.
|
||||
type File struct {
|
||||
Id string
|
||||
Location string
|
||||
// FileName — имя, под которым файл лежит: у местной копии это имя, заданное
|
||||
// сервисом, у внешней — ключ объекта. Своего суффикса хранилище к заданному
|
||||
// имени не дописывает: суффикс появляется только у имён, которые оно строит
|
||||
// само из имени отправителя, а это умолчание не применяется.
|
||||
FileName string
|
||||
Size int64
|
||||
CreatedAt time.Time
|
||||
// FileName — имя, под которым файл лежит: имя задаёт сервис. Своего суффикса
|
||||
// хранилище к заданному имени не дописывает: суффикс появляется только у
|
||||
// имён, которые оно строит само из имени отправителя, а это умолчание не
|
||||
// применяется.
|
||||
FileName string
|
||||
Size int64
|
||||
// Format — расширение без точки, приведённое к нижнему регистру. Наружу оно
|
||||
// выходит только через метку метрики, приведённую к перечню известных.
|
||||
Format string
|
||||
// DurationMs — длительность записи, если её удалось прочитать.
|
||||
DurationMs int64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/clock"
|
||||
)
|
||||
|
||||
type TranscribeJob struct {
|
||||
Id string
|
||||
State string
|
||||
// OwnerID — учётная запись, от имени которой запись принята. Пуст у записей
|
||||
// из Telegram: связи чата с учётной записью сервис не ведёт. Назначается
|
||||
// один раз, при приёме, и у записи, где он есть, больше не меняется.
|
||||
OwnerID *string
|
||||
Source string
|
||||
FileID *string
|
||||
ErrorText *string
|
||||
AcquisitionID *string
|
||||
AcquireTime *time.Time
|
||||
DelayTime *time.Time
|
||||
Attempts int // Число попыток: растёт при захвате, обнуляется на шаге без отказа
|
||||
RecognitionOpID *string // ID операции распознавания в Yandex Cloud
|
||||
TranscriptionText *string // Результат распознавания
|
||||
TgChatId *int64 // Telegram: в какой чат отправить результат распознавания
|
||||
TgReplyMessageId *int // Telegram: с каким сообщением связать результат распознавания
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
StateCreated = "created"
|
||||
StateConverted = "converted"
|
||||
StateTranscribe = "transcribe"
|
||||
StateDone = "done"
|
||||
StateFailed = "failed"
|
||||
// StateDead — задача, которую мы повторяли и перестали. От `failed` она
|
||||
// отличается тем, чей это приговор: в `failed` задачу переводит шаг,
|
||||
// рассудивший об этой записи окончательно, а сюда она уходит без такого
|
||||
// суждения. Ни один шаг конвейера в неё не переводит сам.
|
||||
StateDead = "dead"
|
||||
)
|
||||
|
||||
const (
|
||||
SourceUnknown = "unknown"
|
||||
SourceApi = "api"
|
||||
SourceTelegram = "telegram"
|
||||
)
|
||||
|
||||
// Переводит задачу в новое состояние, при этом очищает все
|
||||
// служебные поля предыдущего состояния, как-то время задержки, информацию о воркере и тд
|
||||
func (j *TranscribeJob) MoveToState(state string) {
|
||||
j.State = state
|
||||
j.DelayTime = nil
|
||||
j.AcquisitionID = nil
|
||||
j.AcquireTime = nil
|
||||
// Шаг, дошедший до перехода, завершился без отказа, а попытки считают
|
||||
// именно отказавшие: иначе задача, прошедшая конвейер целиком, накопила бы
|
||||
// их поштучно и умерла бы здоровой.
|
||||
j.Attempts = 0
|
||||
j.UpdatedAt = clock.Now()
|
||||
}
|
||||
|
||||
func (j *TranscribeJob) MoveToStateAndDelay(state string, delay *time.Time) {
|
||||
j.MoveToState(state)
|
||||
j.DelayTime = delay
|
||||
j.UpdatedAt = clock.Now()
|
||||
}
|
||||
|
||||
func (j *TranscribeJob) Done(transcriptionText string) {
|
||||
j.MoveToState(StateDone)
|
||||
j.TranscriptionText = &transcriptionText
|
||||
}
|
||||
|
||||
func (j *TranscribeJob) Fail(errText string) {
|
||||
j.MoveToState(StateFailed)
|
||||
j.ErrorText = &errText
|
||||
}
|
||||
|
||||
// RetryAfter освобождает отказавшую задачу для повтора: захват снимается,
|
||||
// пауза ставится, а число попыток сохраняется — по нему растёт пауза и
|
||||
// наступает предел.
|
||||
func (j *TranscribeJob) RetryAfter(delay time.Time) {
|
||||
j.AcquisitionID = nil
|
||||
j.AcquireTime = nil
|
||||
j.DelayTime = &delay
|
||||
j.UpdatedAt = clock.Now()
|
||||
}
|
||||
|
||||
// Die переводит задачу, исчерпавшую попытки, в состояние «мертва». Число
|
||||
// попыток при этом сохраняется: по нему видно, сколько раз мы пробовали, а
|
||||
// возвращает задачу в работу владелец правкой состояния.
|
||||
func (j *TranscribeJob) Die(errText string) {
|
||||
attempts := j.Attempts
|
||||
j.MoveToState(StateDead)
|
||||
j.Attempts = attempts
|
||||
j.ErrorText = &errText
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package entity
|
||||
|
||||
// RecognitionStatus представляет статус операции транскрипции
|
||||
import "time"
|
||||
|
||||
// RecognitionStatus представляет статус операции распознавания у провайдера.
|
||||
type RecognitionStatus int
|
||||
|
||||
const (
|
||||
@@ -26,7 +28,7 @@ func (s RecognitionStatus) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// RecognitionResult представляет результат операции транскрипции
|
||||
// RecognitionResult представляет исход опроса операции распознавания.
|
||||
type RecognitionResult struct {
|
||||
Status RecognitionStatus
|
||||
Error string // Текст ошибки (заполняется при StatusFailed)
|
||||
@@ -76,3 +78,43 @@ func (r *RecognitionResult) GetError() string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Recognition — попытка распознавания у внешнего провайдера.
|
||||
//
|
||||
// Всё провайдерское живёт здесь, а не колонками записи: идентификатор операции —
|
||||
// самое провайдерское, что есть в модели, а копия аудио во внешнем хранилище
|
||||
// существует только потому, что сегодняшний провайдер читает запись по адресу.
|
||||
// Другой провайдер её не потребует, и смена провайдера не трогает доменную
|
||||
// сущность вовсе.
|
||||
//
|
||||
// Сырой ответ хранится вложением, а не колонкой этой строки: шаг опроса читает
|
||||
// её раз в несколько секунд, а хранилище читает запись целиком — ответ на
|
||||
// многочасовую запись ехал бы в память при каждом опросе. Хранится он потому,
|
||||
// что результат операции у провайдера не переспрашивается.
|
||||
type Recognition struct {
|
||||
Id string
|
||||
RecordID string
|
||||
Provider string
|
||||
Model string
|
||||
// ExternalID — идентификатор операции у провайдера. Заводится **до**
|
||||
// обращения к нему: окно между ответом провайдера и записью идентификатора —
|
||||
// то место, где теряется оплаченное.
|
||||
ExternalID string
|
||||
// SourceURI — адрес, по которому провайдер читает аудио.
|
||||
SourceURI string
|
||||
StartedAt *time.Time
|
||||
FinishedAt *time.Time
|
||||
}
|
||||
|
||||
// RecognitionOutcome — доменный результат распознавания, каким его отдаёт
|
||||
// адаптер. Формата провайдера здесь нет: разбор потока — обязанность адаптера, и
|
||||
// ни один шаг конвейера не знает, каким потоком и какими полями провайдер
|
||||
// отвечает.
|
||||
type RecognitionOutcome struct {
|
||||
// Replicas — реплики со временем от начала записи.
|
||||
Replicas []Replica
|
||||
// PlainText — плоский текст расшифровки.
|
||||
PlainText string
|
||||
// Raw — ответ провайдера целиком, как он пришёл, на хранение вложением.
|
||||
Raw []byte
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package entity
|
||||
|
||||
// Источник события журнала записи.
|
||||
const (
|
||||
// EventOriginPipeline — событие произвёл шаг конвейера.
|
||||
EventOriginPipeline = "pipeline"
|
||||
// EventOriginHuman — событие произвёл человек: перезапуск виден в журнале с
|
||||
// указанием, кто его сделал.
|
||||
EventOriginHuman = "human"
|
||||
)
|
||||
|
||||
// Исход события.
|
||||
const (
|
||||
EventOutcomeDone = "done"
|
||||
EventOutcomeFailed = "failed"
|
||||
EventOutcomeHalted = "halted"
|
||||
EventOutcomeResumed = "resumed"
|
||||
)
|
||||
|
||||
// AllEventOrigins — закрытый перечень источников для схемы хранилища.
|
||||
func AllEventOrigins() []string {
|
||||
return []string{EventOriginPipeline, EventOriginHuman}
|
||||
}
|
||||
|
||||
// AllEventOutcomes — закрытый перечень исходов для схемы хранилища.
|
||||
func AllEventOutcomes() []string {
|
||||
return []string{EventOutcomeDone, EventOutcomeFailed, EventOutcomeHalted, EventOutcomeResumed}
|
||||
}
|
||||
|
||||
// RecordEvent — строка журнала событий одной записи.
|
||||
//
|
||||
// Пишется на смену рубежа, на остановку и на снятие остановки — не на каждое
|
||||
// откладывание опроса: часовая запись дала бы сотни строк ни о чём. Ни один шаг
|
||||
// конвейера этот журнал не читает, чтобы решить, что делать дальше: решение
|
||||
// принимается по рубежу записи, и второй источник решения разошёлся бы с первым
|
||||
// молча.
|
||||
//
|
||||
// Содержимое записи сюда не попадает — инвариант приватности действует здесь
|
||||
// наравне с журналом сервиса. Поле текста отказа зовётся `OutcomeText`, а не
|
||||
// `error_text`: последнее имя названо поимённо инвариантом о секрете, и две
|
||||
// колонки с этим именем сделали бы инвариант двусмысленным.
|
||||
type RecordEvent struct {
|
||||
Id string
|
||||
RecordID string
|
||||
Origin string
|
||||
Step string
|
||||
Outcome string
|
||||
OutcomeText string
|
||||
DurationMs int64
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package entity
|
||||
|
||||
// Состояния прежней модели. **Частью модели они не являются** и в перечень
|
||||
// рубежей не входят: цепочку рубежей объявляет `stage.go`, а закрытый перечень
|
||||
// для схемы — `AllStates()`.
|
||||
//
|
||||
// Живут они здесь по одной причине: шаг схемы `202608110001_init.go` заводил
|
||||
// прежнюю коллекцию задач этими значениями, а **применённый шаг схемы не
|
||||
// переписывается** — хранилище считает применённое по имени файла, и правка
|
||||
// сделала бы его другим шагом под прежним именем. Шаг ссылается на эти
|
||||
// константы, значит они обязаны существовать, пока существует он.
|
||||
//
|
||||
// Коллекцию, которую он заводил, удаляет шаг `202608140002`. Ни один живой путь
|
||||
// сервиса этих значений не читает и не пишет; `StateDone` в этом списке нет —
|
||||
// то же слово осталось именем конечного рубежа новой модели.
|
||||
const (
|
||||
StateCreated = "created"
|
||||
StateConverted = "converted"
|
||||
StateTranscribe = "transcribe"
|
||||
StateFailed = "failed"
|
||||
StateDead = "dead"
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// Work — чью работу ждёт запись, стоя на рубеже. От этого зависит предел
|
||||
// простоя: своя работа мерится одним числом, ожидание чужой операции — другим.
|
||||
// Граница проходит по исполнителю, а не по рубежу: число на каждый рубеж
|
||||
// назвало бы разными вещи, различающиеся только им.
|
||||
type Work int
|
||||
|
||||
const (
|
||||
// WorkOwn — работу делаем мы сами.
|
||||
WorkOwn Work = iota
|
||||
// WorkForeign — ждём операцию внешнего сервиса.
|
||||
WorkForeign
|
||||
)
|
||||
|
||||
// Stage — объявление рубежа одним местом.
|
||||
//
|
||||
// Из этого перечня выводятся все потребители: выбор следующего шага, отбор
|
||||
// захвата, срок протухания захвата и предел простоя. Перечислять рубежи порознь
|
||||
// в каждом потребителе нельзя: рубеж, забытый в отборе захвата, не выдаётся ни
|
||||
// одному воркеру никогда, а пустой прогон по инварианту проекта не пишется в
|
||||
// журнал и не считается в метрику — запись встала бы без единого следа.
|
||||
type Stage struct {
|
||||
Name string
|
||||
// Work — чью работу ждём, стоя на этом рубеже.
|
||||
Work Work
|
||||
// AcquireTimeout — срок протухания захвата. Едет с рубежом, а не с воркером:
|
||||
// воркер не привязан к шагу и не знает заранее, что вытянет.
|
||||
AcquireTimeout time.Duration
|
||||
// Terminal — рубеж, из которого запись в работу не берут. Такой рубеж не
|
||||
// подпадает и под предел простоя: стоять в нём запись будет вечно по
|
||||
// построению.
|
||||
Terminal bool
|
||||
}
|
||||
|
||||
// Сроки захвата. Каждый не меньше того, что его шаг может занять на самом
|
||||
// длинном допустимом входе: расчётный потолок записи — шесть часов, и приведение
|
||||
// такой записи идёт дольше часа по построению.
|
||||
const (
|
||||
normalizeAcquireTimeout = 8 * time.Hour
|
||||
submitAcquireTimeout = 8 * time.Hour
|
||||
pollAcquireTimeout = time.Hour
|
||||
finishAcquireTimeout = time.Hour
|
||||
)
|
||||
|
||||
// stages — цепочка рубежей в порядке прохождения.
|
||||
var stages = []Stage{
|
||||
{Name: StateUploaded, Work: WorkOwn, AcquireTimeout: normalizeAcquireTimeout},
|
||||
{Name: StateNormalized, Work: WorkOwn, AcquireTimeout: submitAcquireTimeout},
|
||||
{Name: StateSubmitted, Work: WorkForeign, AcquireTimeout: pollAcquireTimeout},
|
||||
{Name: StateTranscribed, Work: WorkOwn, AcquireTimeout: finishAcquireTimeout},
|
||||
{Name: StateDone, Terminal: true},
|
||||
}
|
||||
|
||||
// WorkingStages — рубежи, с которых запись берут в работу.
|
||||
func WorkingStages() []Stage {
|
||||
out := make([]Stage, 0, len(stages))
|
||||
for _, s := range stages {
|
||||
if !s.Terminal {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// StageByName находит рубеж по имени. Второе значение ложно у рубежа, которого
|
||||
// в цепочке нет: запись с таким рубежом до шага не доходит.
|
||||
func StageByName(name string) (Stage, bool) {
|
||||
for _, s := range stages {
|
||||
if s.Name == name {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
return Stage{}, false
|
||||
}
|
||||
|
||||
// StuckLimits — пределы простоя, приходящие из настроек.
|
||||
type StuckLimits struct {
|
||||
// Own — предел на своей работе.
|
||||
Own time.Duration
|
||||
// Foreign — предел на ожидании чужой операции.
|
||||
Foreign time.Duration
|
||||
}
|
||||
|
||||
// Limit — предел простоя для этого рубежа. У конечного рубежа предела нет:
|
||||
// запись стоит в нём вечно по построению.
|
||||
func (s Stage) Limit(limits StuckLimits) (time.Duration, bool) {
|
||||
if s.Terminal {
|
||||
return 0, false
|
||||
}
|
||||
if s.Work == WorkForeign {
|
||||
return limits.Foreign, true
|
||||
}
|
||||
return limits.Own, true
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package entity
|
||||
|
||||
// StructureVersion — версия вида структуры реплик. Разбор сохранённого ответа
|
||||
// провайдера изменится раньше, чем архив пересчитают, и по номеру видно, какой
|
||||
// разбор её построил.
|
||||
const StructureVersion = 1
|
||||
|
||||
// Replica — одна реплика с временем от начала записи.
|
||||
//
|
||||
// Говорящий сегодня не размечается: связь реплики с разбором говорящего у
|
||||
// провайдера не выяснена. Поле заведено, потому что структура строится из
|
||||
// сохранённого ответа и пересчитается без повторной оплаты, когда связь
|
||||
// выяснится.
|
||||
type Replica struct {
|
||||
StartMs int64 `json:"start_ms"`
|
||||
EndMs int64 `json:"end_ms"`
|
||||
Speaker string `json:"speaker,omitempty"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// Structure — реплики записи со временем. Лежит своей строкой, пара «запись и
|
||||
// версия разбора» уникальна.
|
||||
type Structure struct {
|
||||
Id string
|
||||
RecordID string
|
||||
Version int
|
||||
Replicas []Replica
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package entity
|
||||
|
||||
// Виды текста записи. Расшифровка и вычитанный текст читаются по открытию одной
|
||||
// записи и лежат строками `texts`, а не колонками: колонкой на каждый вид схема
|
||||
// росла бы с каждым новым видом, а необратимый шаг схемы платится за каждую
|
||||
// такую колонку отдельно.
|
||||
const (
|
||||
// TextKindTranscript — сырая расшифровка, как её отдал распознаватель.
|
||||
TextKindTranscript = "transcript"
|
||||
// TextKindLiterary — вычитанный текст. Его считает отдельная задача; здесь
|
||||
// заведено только место, куда он ляжет.
|
||||
TextKindLiterary = "literary"
|
||||
)
|
||||
|
||||
// AllTextKinds — закрытый перечень видов текста для схемы хранилища.
|
||||
func AllTextKinds() []string {
|
||||
return []string{TextKindTranscript, TextKindLiterary}
|
||||
}
|
||||
|
||||
// Text — один вид текста одной записи. Пара «запись и вид» уникальна: повтор
|
||||
// прерванного шага иначе завёл бы второй комплект строк, и вопрос «какой текст
|
||||
// отдавать человеку» стал бы вопросом порядка записи, а не состояния.
|
||||
type Text struct {
|
||||
Id string
|
||||
RecordID string
|
||||
Kind string
|
||||
Contents string
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package entity
|
||||
|
||||
// MaxTopicsPerRecord — потолок числа тем у одной записи. Без него часовой
|
||||
// разговор даёт два десятка тем, и словарь распухает за неделю; это же число
|
||||
// уезжает в запрос к языковой модели.
|
||||
const MaxTopicsPerRecord = 5
|
||||
|
||||
// Topic — тема из словаря одного человека. Пара «владелец и название»
|
||||
// уникальна: словарь тем свой у каждого.
|
||||
//
|
||||
// Коллекцией, а не набором строк в записи, потому что перечень тем человека
|
||||
// нужен целиком перед каждым обращением к модели, а собрать его из наборов строк
|
||||
// можно только перебором всех его записей.
|
||||
//
|
||||
// Ни один шаг этой работы тем не пишет и не читает: место заведено вперёд, чтобы
|
||||
// задача, считающая темы языковой моделью, не платила вторым необратимым шагом
|
||||
// схемы.
|
||||
type Topic struct {
|
||||
Id string
|
||||
OwnerID string
|
||||
Name string
|
||||
}
|
||||
Reference in New Issue
Block a user