Files
tapir/cmd/tapir/processor.go
T
mathiasandClaude Opus 4.8 8c6c7ca947
CI / Lint / Test / Vet (push) Successful in 20s
CI / Build & Import (push) Successful in 11s
CI / Mirror to GitHub (push) Failing after 3s
feat(cmd): shared buildProcessor + wire immediate web summarization
Extract the engine wiring (YouTube source, AI-router summarizer, store sink)
into buildProcessor, shared by cmdRun and cmdServe. It returns (nil, nil) — not
an error — on incomplete config, which is the queue-only fallback for serve.
engineProcessor adapts the engine to web.Processor: load the video row, run the
engine, clear the manual queue flag on a produced summary (mirrors the runner).
cmdServe wires it onto web.App.Processor; cmdRun reuses buildProcessor so the
wiring is no longer duplicated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 19:50:06 +02:00

87 lines
3.1 KiB
Go

package main
import (
"context"
"fmt"
"gitea.d-ma.be/mathias/tapir/internal/adapters/llm"
"gitea.d-ma.be/mathias/tapir/internal/adapters/secrets"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/adapters/summarizer"
"gitea.d-ma.be/mathias/tapir/internal/adapters/youtube"
"gitea.d-ma.be/mathias/tapir/internal/config"
"gitea.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/usecase"
)
// buildProcessor wires the summarization engine — YouTube source (captions-first),
// AI-router summarizer, store sink — shared by `tapir run` and the web
// "Summarize now" path so the wiring lives in one place. It returns (nil, nil) —
// not an error — when the config cannot support live summarization (no gateway
// URL, no YouTube client credentials, or no secrets file). That nil is the
// queue-only fallback: the web UI keeps working (the button just queues) and
// `tapir run` reports the gap via its own ValidateForRun. Missing engine config
// is never an error here.
func buildProcessor(cfg config.Config, st *store.Store) (*usecase.Engine, error) {
if cfg.GatewayURL == "" || cfg.YTClientID == "" || cfg.YTClientSecret == "" || cfg.SecretsFile == "" {
return nil, nil
}
secretStore := secrets.NewFileStore(cfg.SecretsFile)
src := youtube.New(youtube.Config{
ClientID: cfg.YTClientID,
ClientSecret: cfg.YTClientSecret,
TokenSecretRef: cfg.YTTokenRef,
PreferredLanguages: []string{"en"},
}, secretStore)
// Local Primary only; no BYO fallback for the demo (fallback nil).
primary := summarizer.Endpoint{
Client: llm.New(cfg.GatewayURL, cfg.GatewayKey, cfg.SummarizerModel, cfg.SummarizerTimeout),
Provider: "local",
Model: cfg.SummarizerModel,
}
sum := summarizer.New(primary, nil)
return usecase.NewEngine(src, sum, st), nil
}
// engineProcessor adapts the engine (which works in terms of a domain.Video) to
// the web.Processor port (which works in terms of a stored video id): it loads the
// video row, runs the engine, and — on a produced summary — clears the manual
// queue flag, mirroring the runner so the video is not re-summarized on the next
// `tapir run` and the UI drops the "Queued" chip. A skip (no transcript) leaves
// the flag set so a later run can retry.
type engineProcessor struct {
engine *usecase.Engine
store *store.Store
}
func (p *engineProcessor) ProcessVideo(ctx context.Context, userID, videoID string) error {
row, err := p.store.GetVideoRow(ctx, userID, videoID)
if err != nil {
return fmt.Errorf("load video %q: %w", videoID, err)
}
v := domain.Video{
ID: row.VideoID,
UserID: userID,
Provider: domain.Provider(row.Channel),
ProviderVideoID: row.ProviderVideoID,
Title: row.Title,
URL: row.URL,
PublishedAt: row.PublishedAt,
}
res, err := p.engine.ProcessNewVideo(ctx, v)
if err != nil {
return fmt.Errorf("process video %q: %w", videoID, err)
}
if res.Summary != nil {
if err := p.store.ClearSummarizeRequested(ctx, userID, videoID); err != nil {
return fmt.Errorf("clear summarize flag %q: %w", videoID, err)
}
}
return nil
}