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>
87 lines
3.1 KiB
Go
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
|
|
}
|