feat(config): add OnboardSummarizerModel + OnboardMaxVideoSeconds (ADR-028)
Two knobs for the onboarding-burst quality work: - TAPIR_ONBOARD_SUMMARIZER_MODEL (default iguana/gemma4-26b): the stronger model the burst leads its chain with; empty collapses the burst onto the shared processor (lookupOr, so explicit-empty disables). - TAPIR_ONBOARD_MAX_VIDEO_SECONDS (default 14400/4h): upper duration bound for burst picks; 0 disables, negative clamps to 0. Table-driven tests cover defaults, explicit, disable, and invalid input.
This commit is contained in:
+67
-40
@@ -120,6 +120,21 @@ type Config struct {
|
|||||||
// caption rate gate (ADR-014) — the cap bounds count, never the pacing. Default 3.
|
// caption rate gate (ADR-014) — the cap bounds count, never the pacing. Default 3.
|
||||||
OnboardSummarizeCount int
|
OnboardSummarizeCount int
|
||||||
|
|
||||||
|
// OnboardSummarizerModel is the summarizer alias the connect-time burst leads
|
||||||
|
// its chain with (ADR-028) — a stronger model is affordable on the ≤3 summaries
|
||||||
|
// that form a new user's first impression. It heads a burst-specific chain;
|
||||||
|
// the standard chain (ADR-022) follows as resilience. Empty (or equal to
|
||||||
|
// SummarizerModel) collapses the burst back onto the shared processor — the
|
||||||
|
// reversibility lever. Default iguana/gemma4-26b (the brain-validated model).
|
||||||
|
OnboardSummarizerModel string
|
||||||
|
|
||||||
|
// OnboardMaxVideoSeconds upper-bounds the duration of a video the onboarding
|
||||||
|
// burst will pick (ADR-028), so the burst does not spend a scarce caption fetch
|
||||||
|
// on a multi-hour livestream VOD that passed the live filter once it ended. Only
|
||||||
|
// a KNOWN duration outside [MinVideoSeconds, this] is dropped; a NULL/unknown
|
||||||
|
// duration is kept (degrade-open). 0 disables the upper bound. Default 14400 (4h).
|
||||||
|
OnboardMaxVideoSeconds int
|
||||||
|
|
||||||
// DiscoveryInterval, when > 0, makes `serve` run in-process scheduled discovery
|
// DiscoveryInterval, when > 0, makes `serve` run in-process scheduled discovery
|
||||||
// for ALL users on that cadence (ADR-018). Zero/unset = disabled, so dev and
|
// for ALL users on that cadence (ADR-018). Zero/unset = disabled, so dev and
|
||||||
// tests never auto-fetch. Single-replica assumption — see cmdServe.
|
// tests never auto-fetch. Single-replica assumption — see cmdServe.
|
||||||
@@ -149,26 +164,28 @@ func (c Config) DexConfigured() bool { return strings.TrimSpace(c.OIDCIssuer) !=
|
|||||||
|
|
||||||
// Defaults (see docs/homelab-integration.md). All overridable via env.
|
// Defaults (see docs/homelab-integration.md). All overridable via env.
|
||||||
const (
|
const (
|
||||||
defaultGatewayURL = "http://koala:30401/v1"
|
defaultGatewayURL = "http://koala:30401/v1"
|
||||||
defaultSummarizerModel = "koala/phi4-mini"
|
defaultSummarizerModel = "koala/phi4-mini"
|
||||||
defaultFallbackModel = "iguana/gemma4-26b"
|
defaultFallbackModel = "iguana/gemma4-26b"
|
||||||
defaultCloudFallbackModel = "berget/mistral-small"
|
defaultCloudFallbackModel = "berget/mistral-small"
|
||||||
defaultSummaryMaxTokens = 1500
|
defaultSummaryMaxTokens = 1500
|
||||||
defaultMaxTranscriptChars = 18000
|
defaultMaxTranscriptChars = 18000
|
||||||
defaultMinVideoSeconds = 60
|
defaultMinVideoSeconds = 60
|
||||||
defaultCaptionlessThreshold = 5
|
defaultCaptionlessThreshold = 5
|
||||||
defaultCaptionlessWindow = 14 * 24 * time.Hour
|
defaultCaptionlessWindow = 14 * 24 * time.Hour
|
||||||
defaultSummarizerTimeout = 5 * time.Minute
|
defaultSummarizerTimeout = 5 * time.Minute
|
||||||
defaultYTTokenRef = "youtube/refresh_token"
|
defaultYTTokenRef = "youtube/refresh_token"
|
||||||
defaultYTConnectRedirectURL = "https://tapir.d-ma.be/oauth/youtube/callback"
|
defaultYTConnectRedirectURL = "https://tapir.d-ma.be/oauth/youtube/callback"
|
||||||
defaultOAuthRedirectAddr = "localhost:8080"
|
defaultOAuthRedirectAddr = "localhost:8080"
|
||||||
defaultHTTPAddr = ":8080"
|
defaultHTTPAddr = ":8080"
|
||||||
defaultFetchBackoff = time.Hour
|
defaultFetchBackoff = time.Hour
|
||||||
defaultFetchRate = 2 * time.Second
|
defaultFetchRate = 2 * time.Second
|
||||||
defaultPublicURL = "https://tapir.d-ma.be"
|
defaultPublicURL = "https://tapir.d-ma.be"
|
||||||
defaultAutoSummarizeWindow = 7 * 24 * time.Hour
|
defaultAutoSummarizeWindow = 7 * 24 * time.Hour
|
||||||
defaultOnboardSummarizeCount = 3
|
defaultOnboardSummarizeCount = 3
|
||||||
maxOnboardSummarizeCount = 5
|
maxOnboardSummarizeCount = 5
|
||||||
|
defaultOnboardSummarizerModel = "iguana/gemma4-26b"
|
||||||
|
defaultOnboardMaxVideoSeconds = 14400 // 4h
|
||||||
)
|
)
|
||||||
|
|
||||||
// Load reads the environment into a Config, applying defaults. It does not
|
// Load reads the environment into a Config, applying defaults. It does not
|
||||||
@@ -177,26 +194,27 @@ const (
|
|||||||
// it needs.
|
// it needs.
|
||||||
func Load() (Config, error) {
|
func Load() (Config, error) {
|
||||||
c := Config{
|
c := Config{
|
||||||
UserID: os.Getenv("TAPIR_USER_ID"),
|
UserID: os.Getenv("TAPIR_USER_ID"),
|
||||||
GatewayURL: envOr("TAPIR_GATEWAY_URL", defaultGatewayURL),
|
GatewayURL: envOr("TAPIR_GATEWAY_URL", defaultGatewayURL),
|
||||||
GatewayKey: os.Getenv("TAPIR_GATEWAY_KEY"),
|
GatewayKey: os.Getenv("TAPIR_GATEWAY_KEY"),
|
||||||
SummarizerModel: envOr("TAPIR_SUMMARIZER_MODEL", defaultSummarizerModel),
|
SummarizerModel: envOr("TAPIR_SUMMARIZER_MODEL", defaultSummarizerModel),
|
||||||
FallbackModel: lookupOr("TAPIR_FALLBACK_MODEL", defaultFallbackModel),
|
OnboardSummarizerModel: lookupOr("TAPIR_ONBOARD_SUMMARIZER_MODEL", defaultOnboardSummarizerModel),
|
||||||
CloudFallbackModel: lookupOr("TAPIR_CLOUD_FALLBACK_MODEL", defaultCloudFallbackModel),
|
FallbackModel: lookupOr("TAPIR_FALLBACK_MODEL", defaultFallbackModel),
|
||||||
DBDSN: os.Getenv("TAPIR_DB_DSN"),
|
CloudFallbackModel: lookupOr("TAPIR_CLOUD_FALLBACK_MODEL", defaultCloudFallbackModel),
|
||||||
YTClientID: os.Getenv("TAPIR_YT_CLIENT_ID"),
|
DBDSN: os.Getenv("TAPIR_DB_DSN"),
|
||||||
YTClientSecret: os.Getenv("TAPIR_YT_CLIENT_SECRET"),
|
YTClientID: os.Getenv("TAPIR_YT_CLIENT_ID"),
|
||||||
YTTokenRef: envOr("TAPIR_YT_TOKEN_REF", defaultYTTokenRef),
|
YTClientSecret: os.Getenv("TAPIR_YT_CLIENT_SECRET"),
|
||||||
YTConnectRedirectURL: envOr("TAPIR_YT_CONNECT_REDIRECT_URL", defaultYTConnectRedirectURL),
|
YTTokenRef: envOr("TAPIR_YT_TOKEN_REF", defaultYTTokenRef),
|
||||||
SecretsFile: envOr("TAPIR_SECRETS_FILE", defaultSecretsFile()),
|
YTConnectRedirectURL: envOr("TAPIR_YT_CONNECT_REDIRECT_URL", defaultYTConnectRedirectURL),
|
||||||
OAuthRedirectAddr: envOr("TAPIR_OAUTH_REDIRECT_ADDR", defaultOAuthRedirectAddr),
|
SecretsFile: envOr("TAPIR_SECRETS_FILE", defaultSecretsFile()),
|
||||||
HTTPAddr: envOr("TAPIR_HTTP_ADDR", defaultHTTPAddr),
|
OAuthRedirectAddr: envOr("TAPIR_OAUTH_REDIRECT_ADDR", defaultOAuthRedirectAddr),
|
||||||
PublicURL: envOr("TAPIR_PUBLIC_URL", defaultPublicURL),
|
HTTPAddr: envOr("TAPIR_HTTP_ADDR", defaultHTTPAddr),
|
||||||
OIDCIssuer: os.Getenv("TAPIR_OIDC_ISSUER"),
|
PublicURL: envOr("TAPIR_PUBLIC_URL", defaultPublicURL),
|
||||||
DexClientID: os.Getenv("TAPIR_DEX_CLIENT_ID"),
|
OIDCIssuer: os.Getenv("TAPIR_OIDC_ISSUER"),
|
||||||
DexClientSecret: os.Getenv("TAPIR_DEX_CLIENT_SECRET"),
|
DexClientID: os.Getenv("TAPIR_DEX_CLIENT_ID"),
|
||||||
OIDCRedirectURL: os.Getenv("TAPIR_OIDC_REDIRECT_URL"),
|
DexClientSecret: os.Getenv("TAPIR_DEX_CLIENT_SECRET"),
|
||||||
SessionSecret: os.Getenv("TAPIR_SESSION_SECRET"),
|
OIDCRedirectURL: os.Getenv("TAPIR_OIDC_REDIRECT_URL"),
|
||||||
|
SessionSecret: os.Getenv("TAPIR_SESSION_SECRET"),
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout, err := durationOr("TAPIR_SUMMARIZER_TIMEOUT", defaultSummarizerTimeout)
|
timeout, err := durationOr("TAPIR_SUMMARIZER_TIMEOUT", defaultSummarizerTimeout)
|
||||||
@@ -286,6 +304,15 @@ func Load() (Config, error) {
|
|||||||
}
|
}
|
||||||
c.OnboardSummarizeCount = onboard
|
c.OnboardSummarizeCount = onboard
|
||||||
|
|
||||||
|
onboardMax, err := intOr("TAPIR_ONBOARD_MAX_VIDEO_SECONDS", defaultOnboardMaxVideoSeconds)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
if onboardMax < 0 {
|
||||||
|
onboardMax = 0
|
||||||
|
}
|
||||||
|
c.OnboardMaxVideoSeconds = onboardMax
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,3 +214,62 @@ func TestValidateForAuth_PassesWhenComplete(t *testing.T) {
|
|||||||
t.Errorf("ValidateForAuth: unexpected error %v", err)
|
t.Errorf("ValidateForAuth: unexpected error %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoad_OnboardSummarizerModel(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name, env string
|
||||||
|
set bool
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"default", "", false, defaultOnboardSummarizerModel},
|
||||||
|
{"explicit", "koala/some-model", true, "koala/some-model"},
|
||||||
|
{"empty disables (collapses to shared processor)", "", true, ""},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
env := map[string]string{}
|
||||||
|
if c.set {
|
||||||
|
env["TAPIR_ONBOARD_SUMMARIZER_MODEL"] = c.env
|
||||||
|
}
|
||||||
|
setEnv(t, env)
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.OnboardSummarizerModel != c.want {
|
||||||
|
t.Fatalf("OnboardSummarizerModel = %q, want %q", cfg.OnboardSummarizerModel, c.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad_OnboardMaxVideoSeconds(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name, env string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"default", "", defaultOnboardMaxVideoSeconds},
|
||||||
|
{"explicit", "7200", 7200},
|
||||||
|
{"zero disables", "0", 0},
|
||||||
|
{"negative clamps to zero", "-9", 0},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
setEnv(t, map[string]string{"TAPIR_ONBOARD_MAX_VIDEO_SECONDS": c.env})
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.OnboardMaxVideoSeconds != c.want {
|
||||||
|
t.Fatalf("OnboardMaxVideoSeconds = %d, want %d", cfg.OnboardMaxVideoSeconds, c.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad_OnboardMaxVideoSecondsInvalid(t *testing.T) {
|
||||||
|
setEnv(t, map[string]string{"TAPIR_ONBOARD_MAX_VIDEO_SECONDS": "long"})
|
||||||
|
if _, err := Load(); err == nil {
|
||||||
|
t.Fatal("Load: want error for non-numeric TAPIR_ONBOARD_MAX_VIDEO_SECONDS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user