внутренняя модель перестроена вокруг аудиозаписи
- 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
|
||||
}
|
||||
Reference in New Issue
Block a user