diff --git a/internal/adapters/store/uploaded_transcripts.go b/internal/adapters/store/uploaded_transcripts.go index 9e2cc81..c5820fe 100644 --- a/internal/adapters/store/uploaded_transcripts.go +++ b/internal/adapters/store/uploaded_transcripts.go @@ -52,12 +52,20 @@ func (s *Store) CompleteUpload(ctx context.Context, userID, id string, t domain. if err != nil { return fmt.Errorf("store: complete upload: marshal segments: %w", err) } + // A nil Reconciliation must be SQL NULL, not the JSON text "null": the page decides + // whether to claim a second opinion by whether this column is NULL. + var rec []byte + if t.Reconciliation != nil { + if rec, err = json.Marshal(t.Reconciliation); err != nil { + return fmt.Errorf("store: complete upload: marshal reconciliation: %w", err) + } + } return s.updateUpload(ctx, userID, id, "complete upload", `UPDATE uploaded_transcripts SET status = 'done', content = $3, segments = $4, endpoint = $5, - audio_seconds = $6, error_kind = NULL, finished_at = NOW() + audio_seconds = $6, reconciliation = $7, error_kind = NULL, finished_at = NOW() WHERE id = $1 AND user_id = $2`, - t.Text, segs, t.Endpoint, t.AudioSeconds) + t.Text, segs, t.Endpoint, t.AudioSeconds, rec) } // FailUpload moves an upload to failed with a typed reason. @@ -157,19 +165,19 @@ func (s *Store) FailInterruptedUploads(ctx context.Context) (int, error) { } const uploadSelect = `SELECT id, user_id, title, status, content, segments, endpoint, - audio_seconds, error_kind, had_video, created_at, finished_at FROM uploaded_transcripts` + audio_seconds, error_kind, had_video, reconciliation, created_at, finished_at FROM uploaded_transcripts` // scanUpload scans one uploadSelect row. withText controls whether the transcript // body and segments are kept (GetUpload) or dropped (ListUploads). func scanUpload(row pgx.Row, u *domain.UploadedTranscript, withText bool) error { var ( status, errKind, endpoint, content *string - segments []byte + segments, reconciliation []byte seconds *float64 finished *time.Time ) if err := row.Scan(&u.ID, &u.UserID, &u.Title, &status, &content, &segments, &endpoint, - &seconds, &errKind, &u.HadVideo, &u.CreatedAt, &finished); err != nil { + &seconds, &errKind, &u.HadVideo, &reconciliation, &u.CreatedAt, &finished); err != nil { return err } u.Status = domain.UploadStatus(deref(status)) @@ -184,6 +192,13 @@ func scanUpload(row pgx.Row, u *domain.UploadedTranscript, withText bool) error if !withText { return nil } + if len(reconciliation) > 0 { + var r domain.Reconciliation + if err := json.Unmarshal(reconciliation, &r); err != nil { + return fmt.Errorf("unmarshal reconciliation: %w", err) + } + u.Reconciliation = &r + } u.Content = deref(content) if len(segments) > 0 { if err := json.Unmarshal(segments, &u.Segments); err != nil { diff --git a/internal/domain/domain.go b/internal/domain/domain.go index afceb7f..2952d6d 100644 --- a/internal/domain/domain.go +++ b/internal/domain/domain.go @@ -214,7 +214,10 @@ type UploadedTranscript struct { // HadVideo records that the uploaded file carried a video track. Only its audio // was used and the video was discarded; the page says so. It is set at upload // and never cleared (ADR-033). - HadVideo bool - CreatedAt time.Time - FinishedAt time.Time // zero until the job reaches done or failed + HadVideo bool + // Reconciliation is how a merged transcript came about, or nil for a single + // engine (ADR-034). Set by Get, not by List. + Reconciliation *Reconciliation + CreatedAt time.Time + FinishedAt time.Time // zero until the job reaches done or failed }