Files
transcriber/internal/adapter/repo/pocketbase/record_mapping.go
T
av 1576d06735 внутренняя модель перестроена вокруг аудиозаписи
- audiorecords вместо transcribe_jobs: приложения (texts, structures,
  recognitions, record_events, topics) живут своими коллекциями, ссылки на
  исходник и на приведённую копию перестали переставляться
- рубеж называет достигнутое, отказ стал признаком остановки с причиной, а
  сторожей стало двое: число отказов и время в рубеже
- воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг
  выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
2026-08-14 20:20:33 +03:00

152 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package pocketbase
import (
"time"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/types"
"git.vakhrushev.me/av/transcriber/internal/entity"
)
// Отображение аудиозаписи в запись коллекции и обратно живёт одним местом.
//
// Мест стало **два** вместо прежних четырёх: захват больше не перечисляет
// колонки поимённо, а возвращает идентификатор и признак своего захвата.
// Инвариант проекта о колонках очереди этим съёживается и перестаёт расти с
// моделью — иначе каждая новая колонка записи попадала бы под него.
// applyOwnedByPipeline кладёт в запись только те поля, которыми распоряжается
// конвейер. Поля, которые он не меняет никогда — владелец, вход, заголовок,
// краткое описание, темы и адресат ответа, — не трогаются вовсе.
//
// Разрез нужен потому, что шаг держит запись снимком с момента захвата и до
// своего сохранения, а это часы. Всё, что владелец правил в панели за это время,
// безусловная запись снимка стёрла бы молча: ни строки в журнале, ни отказа в
// панели — владелец видел бы успешное сохранение и был бы уверен, что правка на
// месте.
func applyOwnedByPipeline(record *core.Record, r *entity.AudioRecord) {
record.Set("state", r.State)
record.Set("state_entered_at", dateOrEmpty(&r.StateEnteredAt))
record.Set("halted_at", dateOrEmpty(r.HaltedAt))
record.Set("halt_reason", derefString(r.HaltReason))
record.Set("error_text", derefString(r.ErrorText))
record.Set("acquisition_id", derefString(r.AcquisitionID))
record.Set("acquire_expires_at", dateOrEmpty(r.AcquireExpiresAt))
record.Set("delay_time", dateOrEmpty(r.DelayTime))
record.Set("attempts", r.Attempts)
record.Set("original_file", derefString(r.OriginalFileID))
record.Set("normalized_file", derefString(r.NormalizedFileID))
record.Set("transcript_text", derefString(r.TranscriptTextID))
record.Set("literary_text", derefString(r.LiteraryTextID))
record.Set("structure", derefString(r.StructureID))
record.Set("recognition", derefString(r.RecognitionID))
}
// applyToRecord кладёт запись целиком — это заведение, и спорить за поля здесь
// не с кем.
func applyToRecord(record *core.Record, r *entity.AudioRecord) {
applyOwnedByPipeline(record, r)
// Владелец кладётся только здесь, при заведении. В applyOwnedByPipeline его
// нет намеренно: конвейер владельца не назначает и не меняет, а снимок шага,
// записанный поверх, стёр бы его молча.
record.Set("owner", derefString(r.OwnerID))
record.Set("source", r.Source)
record.Set("title", derefString(r.Title))
record.Set("brief", derefString(r.Brief))
record.Set("tg_chat_id", derefInt64(r.TgChatId))
record.Set("tg_reply_message_id", derefInt(r.TgReplyMessageId))
}
func recordToAudioRecord(record *core.Record) *entity.AudioRecord {
return &entity.AudioRecord{
Id: record.Id,
OwnerID: nilIfEmpty(record.GetString("owner")),
Source: record.GetString("source"),
Title: nilIfEmpty(record.GetString("title")),
Brief: nilIfEmpty(record.GetString("brief")),
State: record.GetString("state"),
StateEnteredAt: record.GetDateTime("state_entered_at").Time(),
HaltedAt: timeOrNil(record.GetDateTime("halted_at")),
HaltReason: nilIfEmpty(record.GetString("halt_reason")),
ErrorText: nilIfEmpty(record.GetString("error_text")),
AcquisitionID: nilIfEmpty(record.GetString("acquisition_id")),
AcquireExpiresAt: timeOrNil(record.GetDateTime("acquire_expires_at")),
DelayTime: timeOrNil(record.GetDateTime("delay_time")),
Attempts: record.GetInt("attempts"),
OriginalFileID: nilIfEmpty(record.GetString("original_file")),
NormalizedFileID: nilIfEmpty(record.GetString("normalized_file")),
TranscriptTextID: nilIfEmpty(record.GetString("transcript_text")),
LiteraryTextID: nilIfEmpty(record.GetString("literary_text")),
StructureID: nilIfEmpty(record.GetString("structure")),
RecognitionID: nilIfEmpty(record.GetString("recognition")),
TgChatId: nilIfZero64(int64(record.GetInt("tg_chat_id"))),
TgReplyMessageId: nilIfZeroInt(record.GetInt("tg_reply_message_id")),
CreatedAt: record.GetDateTime("created").Time(),
UpdatedAt: record.GetDateTime("updated").Time(),
}
}
func derefString(v *string) string {
if v == nil {
return ""
}
return *v
}
func derefInt64(v *int64) int64 {
if v == nil {
return 0
}
return *v
}
func derefInt(v *int) int {
if v == nil {
return 0
}
return *v
}
// dateOrEmpty отдаёт пустое значение вместо нулевой даты: пустая колонка даты в
// хранилище это пустая строка, и она же значит «времени нет».
func dateOrEmpty(v *time.Time) any {
if v == nil || v.IsZero() {
return ""
}
date, err := types.ParseDateTime(*v)
if err != nil {
return ""
}
return date
}
func nilIfEmpty(v string) *string {
if v == "" {
return nil
}
return &v
}
func timeOrNil(v types.DateTime) *time.Time {
if v.IsZero() {
return nil
}
t := v.Time()
return &t
}
func nilIfZero64(v int64) *int64 {
if v == 0 {
return nil
}
return &v
}
func nilIfZeroInt(v int) *int {
if v == 0 {
return nil
}
return &v
}