diff --git a/internal/usecase/engine.go b/internal/usecase/engine.go index b8d0e56..2dafddf 100644 --- a/internal/usecase/engine.go +++ b/internal/usecase/engine.go @@ -44,8 +44,13 @@ func NewEngine(src ports.VideoSource, ai ports.Summarizer, sinks ...ports.Sink) type ProcessResult struct { Video domain.Video Skipped bool - Reason string // set when Skipped (e.g. "no transcript") - Summary *domain.Summary // nil when Skipped + Reason string // set when Skipped (e.g. "no transcript") + // TranscriptSource is how the transcript resolved (or that there was none): + // the domain.TranscriptSource value as a string. The runner reads it to tell a + // permanent absence (SourceNone) from a transient 429 (SourceRateLimited) and + // persist the right transcript_status. Empty when a fetch error short-circuits. + TranscriptSource string + Summary *domain.Summary // nil when Skipped } // ProcessNewVideo runs the core use case for a single video: @@ -59,7 +64,9 @@ func (e *Engine) ProcessNewVideo(ctx context.Context, v domain.Video) (ProcessRe if !t.HasText() { // No usable transcript: record the skip, produce no summary, deliver nothing // (captions-first, ADR-007; the watcher uses this to avoid reprocessing). - return ProcessResult{Video: v, Skipped: true, Reason: "no transcript"}, nil + // Surface the source so the runner separates SourceNone (permanent) from + // SourceRateLimited (retry after a backoff window). + return ProcessResult{Video: v, Skipped: true, Reason: "no transcript", TranscriptSource: string(t.Source)}, nil } sum, err := e.AI.Summarize(ctx, v, t) @@ -76,7 +83,7 @@ func (e *Engine) ProcessNewVideo(ctx context.Context, v domain.Video) (ProcessRe } } - return ProcessResult{Video: v, Summary: &sum}, errors.Join(errs...) + return ProcessResult{Video: v, Summary: &sum, TranscriptSource: string(t.Source)}, errors.Join(errs...) } // ProcessNewVideos walks a user's subscriptions and processes each newly seen