Add Go configuration model
This commit is contained in:
@@ -5,6 +5,9 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
||||
)
|
||||
|
||||
const processNotImplementedMessage = "process command is not implemented yet"
|
||||
@@ -36,18 +39,83 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
cfg, err := config.LoadFromEnv()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "audita process: invalid environment configuration: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
|
||||
fs := flag.NewFlagSet("process", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
|
||||
glossaryPath := fs.String("glossary", "", "Path to glossary YAML file")
|
||||
outputPath := fs.String("output", "", "Path to corrected transcript JSON output file")
|
||||
reportJSONPath := fs.String("report-json", "", "Path to machine-readable report JSON output file")
|
||||
modules := fs.String("modules", "", "Comma-separated module sequence override")
|
||||
modules := fs.String("modules", strings.Join(cfg.Modules, ","), "Comma-separated module sequence override")
|
||||
llmAPIKey := fs.String("llm-api-key", cfg.PrimaryLLM.APIKey, "Primary LLM API key")
|
||||
validationLLMAPIKey := fs.String("validation-llm-api-key", cfg.ValidationLLM.APIKey, "Validation LLM API key")
|
||||
model := fs.String("model", cfg.PrimaryLLM.Model, "Primary LLM model name")
|
||||
validationModel := fs.String("validation-model", cfg.ValidationLLM.Model, "Validation LLM model name")
|
||||
baseURL := fs.String("base-url", cfg.PrimaryLLM.BaseURL, "Primary OpenAI-compatible base URL")
|
||||
validationBaseURL := fs.String("validation-base-url", cfg.ValidationLLM.BaseURL, "Validation OpenAI-compatible base URL")
|
||||
llmTimeoutSeconds := fs.Int("llm-timeout-seconds", cfg.PrimaryLLM.TimeoutSeconds, "Primary LLM timeout in seconds")
|
||||
validationTimeoutSecondsDefault := cfg.PrimaryLLM.TimeoutSeconds
|
||||
if cfg.ValidationLLM.TimeoutSeconds != nil {
|
||||
validationTimeoutSecondsDefault = *cfg.ValidationLLM.TimeoutSeconds
|
||||
}
|
||||
validationLLMTimeoutSeconds := fs.Int("validation-llm-timeout-seconds", validationTimeoutSecondsDefault, "Validation LLM timeout in seconds")
|
||||
maxRetries := fs.Int("max-retries", cfg.PrimaryLLM.MaxRetries, "Maximum structured-output retries")
|
||||
validationMaxRetriesDefault := cfg.PrimaryLLM.MaxRetries
|
||||
if cfg.ValidationLLM.MaxRetries != nil {
|
||||
validationMaxRetriesDefault = *cfg.ValidationLLM.MaxRetries
|
||||
}
|
||||
validationMaxRetries := fs.Int("validation-max-retries", validationMaxRetriesDefault, "Validation structured-output retries")
|
||||
validationMaxPromptTokens := fs.Int("validation-max-prompt-tokens", cfg.ValidationMaxPromptTokens, "Validation max prompt tokens")
|
||||
maxSectionTokens := fs.Int("max-section-tokens", cfg.MaxSectionTokens, "Max section tokens")
|
||||
minSectionTokens := fs.Int("min-section-tokens", cfg.MinSectionTokens, "Min section tokens")
|
||||
targetSectionsDefault := 0
|
||||
if cfg.TargetSections != nil {
|
||||
targetSectionsDefault = *cfg.TargetSections
|
||||
}
|
||||
targetSections := fs.Int("target-sections", targetSectionsDefault, "Target number of transcript sections")
|
||||
glossaryConfidenceThreshold := fs.Float64("glossary-confidence-threshold", cfg.Thresholds.Glossary, "Glossary confidence threshold")
|
||||
grammarConfidenceThreshold := fs.Float64("grammar-confidence-threshold", cfg.Thresholds.Grammar, "Grammar confidence threshold")
|
||||
homophonesConfidenceThreshold := fs.Float64("homophones-confidence-threshold", cfg.Thresholds.Homophones, "Homophones confidence threshold")
|
||||
spokenWordConfidenceThreshold := fs.Float64("spoken-word-confidence-threshold", cfg.Thresholds.SpokenWord, "Spoken-word confidence threshold")
|
||||
normalizeMaxSegmentGap := fs.Float64("normalize-max-segment-gap", cfg.Normalization.MaxSegmentGap, "Maximum same-speaker merge gap")
|
||||
normalizeEllipsisGap := fs.Float64("normalize-ellipsis-gap", cfg.Normalization.EllipsisGap, "Gap threshold for ellipsis insertion")
|
||||
normalizeMaxSegmentDuration := fs.Float64("normalize-max-segment-duration", cfg.Normalization.MaxSegmentDuration, "Maximum merged segment duration")
|
||||
normalizeMaxSegmentTokens := fs.Int("normalize-max-segment-tokens", cfg.Normalization.MaxSegmentTokens, "Maximum merged segment token estimate")
|
||||
workDir := fs.String("work-dir", cfg.WorkDir, "Per-run work directory")
|
||||
workDirRetention := fs.String("work-dir-retention", string(cfg.WorkDirRetention), "Work-dir retention policy: auto|always|never")
|
||||
|
||||
_ = glossaryPath
|
||||
_ = outputPath
|
||||
_ = reportJSONPath
|
||||
_ = modules
|
||||
_ = llmAPIKey
|
||||
_ = validationLLMAPIKey
|
||||
_ = model
|
||||
_ = validationModel
|
||||
_ = baseURL
|
||||
_ = validationBaseURL
|
||||
_ = llmTimeoutSeconds
|
||||
_ = validationLLMTimeoutSeconds
|
||||
_ = maxRetries
|
||||
_ = validationMaxRetries
|
||||
_ = validationMaxPromptTokens
|
||||
_ = maxSectionTokens
|
||||
_ = minSectionTokens
|
||||
_ = targetSections
|
||||
_ = glossaryConfidenceThreshold
|
||||
_ = grammarConfidenceThreshold
|
||||
_ = homophonesConfidenceThreshold
|
||||
_ = spokenWordConfidenceThreshold
|
||||
_ = normalizeMaxSegmentGap
|
||||
_ = normalizeEllipsisGap
|
||||
_ = normalizeMaxSegmentDuration
|
||||
_ = normalizeMaxSegmentTokens
|
||||
_ = workDir
|
||||
_ = workDirRetention
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
@@ -57,6 +125,67 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
||||
return 2
|
||||
}
|
||||
|
||||
overrides := config.CLIOverrides{}
|
||||
fs.Visit(func(f *flag.Flag) {
|
||||
switch f.Name {
|
||||
case "modules":
|
||||
overrides.ModulesCSV = modules
|
||||
case "llm-api-key":
|
||||
overrides.PrimaryLLMAPIKey = llmAPIKey
|
||||
case "validation-llm-api-key":
|
||||
overrides.ValidationLLMAPIKey = validationLLMAPIKey
|
||||
case "model":
|
||||
overrides.PrimaryModel = model
|
||||
case "validation-model":
|
||||
overrides.ValidationModel = validationModel
|
||||
case "base-url":
|
||||
overrides.PrimaryBaseURL = baseURL
|
||||
case "validation-base-url":
|
||||
overrides.ValidationBaseURL = validationBaseURL
|
||||
case "llm-timeout-seconds":
|
||||
overrides.PrimaryLLMTimeoutSeconds = llmTimeoutSeconds
|
||||
case "validation-llm-timeout-seconds":
|
||||
overrides.ValidationLLMTimeoutSeconds = validationLLMTimeoutSeconds
|
||||
case "max-retries":
|
||||
overrides.MaxRetries = maxRetries
|
||||
case "validation-max-retries":
|
||||
overrides.ValidationMaxRetries = validationMaxRetries
|
||||
case "validation-max-prompt-tokens":
|
||||
overrides.ValidationMaxPromptTokens = validationMaxPromptTokens
|
||||
case "max-section-tokens":
|
||||
overrides.MaxSectionTokens = maxSectionTokens
|
||||
case "min-section-tokens":
|
||||
overrides.MinSectionTokens = minSectionTokens
|
||||
case "target-sections":
|
||||
overrides.TargetSections = targetSections
|
||||
case "glossary-confidence-threshold":
|
||||
overrides.GlossaryConfidenceThreshold = glossaryConfidenceThreshold
|
||||
case "grammar-confidence-threshold":
|
||||
overrides.GrammarConfidenceThreshold = grammarConfidenceThreshold
|
||||
case "homophones-confidence-threshold":
|
||||
overrides.HomophonesConfidenceThreshold = homophonesConfidenceThreshold
|
||||
case "spoken-word-confidence-threshold":
|
||||
overrides.SpokenWordConfidenceThreshold = spokenWordConfidenceThreshold
|
||||
case "normalize-max-segment-gap":
|
||||
overrides.NormalizeMaxSegmentGap = normalizeMaxSegmentGap
|
||||
case "normalize-ellipsis-gap":
|
||||
overrides.NormalizeEllipsisGap = normalizeEllipsisGap
|
||||
case "normalize-max-segment-duration":
|
||||
overrides.NormalizeMaxSegmentDuration = normalizeMaxSegmentDuration
|
||||
case "normalize-max-segment-tokens":
|
||||
overrides.NormalizeMaxSegmentTokens = normalizeMaxSegmentTokens
|
||||
case "work-dir":
|
||||
overrides.WorkDir = workDir
|
||||
case "work-dir-retention":
|
||||
overrides.WorkDirRetention = workDirRetention
|
||||
}
|
||||
})
|
||||
|
||||
if err := cfg.ApplyCLIOverrides(overrides); err != nil {
|
||||
fmt.Fprintf(stderr, "audita process: invalid CLI configuration: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
|
||||
remaining := fs.Args()
|
||||
if len(remaining) != 1 {
|
||||
fmt.Fprintln(stderr, "audita process: expected exactly 1 transcript JSON path argument")
|
||||
@@ -121,6 +250,11 @@ func writeProcessUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, " --output <path> Path to corrected transcript JSON output file")
|
||||
fmt.Fprintln(w, " --report-json <path> Path to machine-readable report JSON output file")
|
||||
fmt.Fprintln(w, " --modules <list> Comma-separated module sequence override")
|
||||
fmt.Fprintln(w, " --llm-api-key <key> Primary LLM API key")
|
||||
fmt.Fprintln(w, " --model <name> Primary LLM model")
|
||||
fmt.Fprintln(w, " --base-url <url> Primary OpenAI-compatible API base URL")
|
||||
fmt.Fprintln(w, " --work-dir <path> Per-run work directory")
|
||||
fmt.Fprintln(w, " --work-dir-retention <mode> auto|always|never")
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "Example:")
|
||||
fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json")
|
||||
|
||||
147
internal/core/config/config.go
Normal file
147
internal/core/config/config.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type WorkDirRetention string
|
||||
|
||||
const (
|
||||
WorkDirRetentionAuto WorkDirRetention = "auto"
|
||||
WorkDirRetentionAlways WorkDirRetention = "always"
|
||||
WorkDirRetentionNever WorkDirRetention = "never"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultModulesCSV = "glossary,homophones,glossary,spoken_word,grammar"
|
||||
DefaultPrimaryModel = "openrouter/google/gemma-4-31b-it"
|
||||
DefaultPrimaryBaseURL = "https://openrouter.ai/api/v1"
|
||||
DefaultPrimaryLLMTimeoutSeconds = 600
|
||||
DefaultMaxRetries = 3
|
||||
DefaultValidationMaxPromptTokens = 2048
|
||||
DefaultMaxSectionTokens = 8192
|
||||
DefaultMinSectionTokens = 2048
|
||||
DefaultConfidenceThreshold = 0.8
|
||||
DefaultNormalizeMaxSegmentGap = 4.0
|
||||
DefaultNormalizeEllipsisGap = 3.5
|
||||
DefaultNormalizeMaxSegmentDuration = 60.0
|
||||
DefaultNormalizeMaxSegmentTokens = 2048
|
||||
DefaultWorkDir = "/tmp/audita"
|
||||
DefaultWorkDirRetention WorkDirRetention = WorkDirRetentionAuto
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Modules []string
|
||||
PrimaryLLM LLMConfig
|
||||
ValidationLLM ValidationLLMConfig
|
||||
ValidationMaxPromptTokens int
|
||||
MaxSectionTokens int
|
||||
MinSectionTokens int
|
||||
TargetSections *int
|
||||
Thresholds ConfidenceThresholds
|
||||
Normalization NormalizationConfig
|
||||
WorkDir string
|
||||
WorkDirRetention WorkDirRetention
|
||||
}
|
||||
|
||||
type LLMConfig struct {
|
||||
APIKey string
|
||||
Model string
|
||||
BaseURL string
|
||||
TimeoutSeconds int
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
type ValidationLLMConfig struct {
|
||||
APIKey string
|
||||
Model string
|
||||
BaseURL string
|
||||
TimeoutSeconds *int
|
||||
MaxRetries *int
|
||||
}
|
||||
|
||||
type ConfidenceThresholds struct {
|
||||
Glossary float64
|
||||
Grammar float64
|
||||
Homophones float64
|
||||
SpokenWord float64
|
||||
}
|
||||
|
||||
type NormalizationConfig struct {
|
||||
MaxSegmentGap float64
|
||||
EllipsisGap float64
|
||||
MaxSegmentDuration float64
|
||||
MaxSegmentTokens int
|
||||
}
|
||||
|
||||
func Default() Config {
|
||||
modules, _ := ParseModulesCSV(DefaultModulesCSV)
|
||||
|
||||
return Config{
|
||||
Modules: modules,
|
||||
PrimaryLLM: LLMConfig{
|
||||
Model: DefaultPrimaryModel,
|
||||
BaseURL: DefaultPrimaryBaseURL,
|
||||
TimeoutSeconds: DefaultPrimaryLLMTimeoutSeconds,
|
||||
MaxRetries: DefaultMaxRetries,
|
||||
},
|
||||
ValidationLLM: ValidationLLMConfig{},
|
||||
ValidationMaxPromptTokens: DefaultValidationMaxPromptTokens,
|
||||
MaxSectionTokens: DefaultMaxSectionTokens,
|
||||
MinSectionTokens: DefaultMinSectionTokens,
|
||||
TargetSections: nil,
|
||||
Thresholds: ConfidenceThresholds{
|
||||
Glossary: DefaultConfidenceThreshold,
|
||||
Grammar: DefaultConfidenceThreshold,
|
||||
Homophones: DefaultConfidenceThreshold,
|
||||
SpokenWord: DefaultConfidenceThreshold,
|
||||
},
|
||||
Normalization: NormalizationConfig{
|
||||
MaxSegmentGap: DefaultNormalizeMaxSegmentGap,
|
||||
EllipsisGap: DefaultNormalizeEllipsisGap,
|
||||
MaxSegmentDuration: DefaultNormalizeMaxSegmentDuration,
|
||||
MaxSegmentTokens: DefaultNormalizeMaxSegmentTokens,
|
||||
},
|
||||
WorkDir: DefaultWorkDir,
|
||||
WorkDirRetention: DefaultWorkDirRetention,
|
||||
}
|
||||
}
|
||||
|
||||
func ParseModulesCSV(raw string) ([]string, error) {
|
||||
parts := strings.Split(raw, ",")
|
||||
modules := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
return nil, fmt.Errorf("modules list contains an empty value")
|
||||
}
|
||||
modules = append(modules, trimmed)
|
||||
}
|
||||
if len(modules) == 0 {
|
||||
return nil, fmt.Errorf("modules list must not be empty")
|
||||
}
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
func (c Config) EffectiveValidationLLMConfig() LLMConfig {
|
||||
effective := c.PrimaryLLM
|
||||
|
||||
if c.ValidationLLM.APIKey != "" {
|
||||
effective.APIKey = c.ValidationLLM.APIKey
|
||||
}
|
||||
if c.ValidationLLM.Model != "" {
|
||||
effective.Model = c.ValidationLLM.Model
|
||||
}
|
||||
if c.ValidationLLM.BaseURL != "" {
|
||||
effective.BaseURL = c.ValidationLLM.BaseURL
|
||||
}
|
||||
if c.ValidationLLM.TimeoutSeconds != nil {
|
||||
effective.TimeoutSeconds = *c.ValidationLLM.TimeoutSeconds
|
||||
}
|
||||
if c.ValidationLLM.MaxRetries != nil {
|
||||
effective.MaxRetries = *c.ValidationLLM.MaxRetries
|
||||
}
|
||||
|
||||
return effective
|
||||
}
|
||||
215
internal/core/config/config_test.go
Normal file
215
internal/core/config/config_test.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultConfigValues(t *testing.T) {
|
||||
cfg := Default()
|
||||
|
||||
if got, want := strings.Join(cfg.Modules, ","), DefaultModulesCSV; got != want {
|
||||
t.Fatalf("modules mismatch: got %q want %q", got, want)
|
||||
}
|
||||
if cfg.PrimaryLLM.Model != DefaultPrimaryModel {
|
||||
t.Fatalf("unexpected default primary model: %q", cfg.PrimaryLLM.Model)
|
||||
}
|
||||
if cfg.PrimaryLLM.BaseURL != DefaultPrimaryBaseURL {
|
||||
t.Fatalf("unexpected default primary base url: %q", cfg.PrimaryLLM.BaseURL)
|
||||
}
|
||||
if cfg.PrimaryLLM.TimeoutSeconds != DefaultPrimaryLLMTimeoutSeconds {
|
||||
t.Fatalf("unexpected default timeout seconds: %d", cfg.PrimaryLLM.TimeoutSeconds)
|
||||
}
|
||||
if cfg.PrimaryLLM.MaxRetries != DefaultMaxRetries {
|
||||
t.Fatalf("unexpected default max retries: %d", cfg.PrimaryLLM.MaxRetries)
|
||||
}
|
||||
if cfg.ValidationLLM.TimeoutSeconds != nil {
|
||||
t.Fatalf("expected validation timeout to be unset by default")
|
||||
}
|
||||
if cfg.ValidationLLM.MaxRetries != nil {
|
||||
t.Fatalf("expected validation max retries to be unset by default")
|
||||
}
|
||||
if cfg.TargetSections != nil {
|
||||
t.Fatalf("expected target sections to be unset by default")
|
||||
}
|
||||
if cfg.WorkDir != DefaultWorkDir {
|
||||
t.Fatalf("unexpected default work dir: %q", cfg.WorkDir)
|
||||
}
|
||||
if cfg.WorkDirRetention != DefaultWorkDirRetention {
|
||||
t.Fatalf("unexpected default work dir retention: %q", cfg.WorkDirRetention)
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("default config should validate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromEnvOverridesAndFallback(t *testing.T) {
|
||||
env := map[string]string{
|
||||
"AUDITA_MODEL": "openai/gpt-4.1-mini",
|
||||
"AUDITA_BASE_URL": "https://api.openai.com/v1",
|
||||
"AUDITA_LLM_TIMEOUT_SECONDS": "120",
|
||||
"AUDITA_MAX_RETRIES": "7",
|
||||
"AUDITA_VALIDATION_MAX_PROMPT_TOKENS": "4096",
|
||||
"AUDITA_MAX_SECTION_TOKENS": "9000",
|
||||
"AUDITA_MIN_SECTION_TOKENS": "3000",
|
||||
"AUDITA_TARGET_SECTIONS": "5",
|
||||
"AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD": "0.9",
|
||||
"AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD": "0.7",
|
||||
"AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD": "0.6",
|
||||
"AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD": "0.5",
|
||||
"AUDITA_NORMALIZE_MAX_SEGMENT_GAP": "2.5",
|
||||
"AUDITA_NORMALIZE_ELLIPSIS_GAP": "2.0",
|
||||
"AUDITA_NORMALIZE_MAX_SEGMENT_DURATION": "30.0",
|
||||
"AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS": "1024",
|
||||
"AUDITA_WORK_DIR": "/var/tmp/audita",
|
||||
"AUDITA_WORK_DIR_RETENTION": "always",
|
||||
"OPENROUTER_API_KEY": "fallback-key",
|
||||
}
|
||||
|
||||
cfg, err := loadFromLookup(mapLookup(env))
|
||||
if err != nil {
|
||||
t.Fatalf("loadFromLookup returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.PrimaryLLM.APIKey != "fallback-key" {
|
||||
t.Fatalf("expected OPENROUTER_API_KEY fallback, got %q", cfg.PrimaryLLM.APIKey)
|
||||
}
|
||||
if cfg.PrimaryLLM.Model != env["AUDITA_MODEL"] {
|
||||
t.Fatalf("unexpected model: %q", cfg.PrimaryLLM.Model)
|
||||
}
|
||||
if cfg.PrimaryLLM.BaseURL != env["AUDITA_BASE_URL"] {
|
||||
t.Fatalf("unexpected base url: %q", cfg.PrimaryLLM.BaseURL)
|
||||
}
|
||||
if cfg.TargetSections == nil || *cfg.TargetSections != 5 {
|
||||
t.Fatalf("unexpected target sections: %#v", cfg.TargetSections)
|
||||
}
|
||||
if cfg.WorkDirRetention != WorkDirRetentionAlways {
|
||||
t.Fatalf("unexpected work dir retention: %q", cfg.WorkDirRetention)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromEnvUsesAuditaLLMAPIKeyOverFallback(t *testing.T) {
|
||||
env := map[string]string{
|
||||
"AUDITA_LLM_API_KEY": "primary-key",
|
||||
"OPENROUTER_API_KEY": "fallback-key",
|
||||
}
|
||||
|
||||
cfg, err := loadFromLookup(mapLookup(env))
|
||||
if err != nil {
|
||||
t.Fatalf("loadFromLookup returned error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.PrimaryLLM.APIKey != "primary-key" {
|
||||
t.Fatalf("expected AUDITA_LLM_API_KEY to win, got %q", cfg.PrimaryLLM.APIKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyCLIOverridesPrecedence(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.PrimaryLLM.Model = "env-model"
|
||||
cfg.WorkDir = "/env/work"
|
||||
|
||||
model := "cli-model"
|
||||
workDir := "/cli/work"
|
||||
modules := "grammar"
|
||||
overrides := CLIOverrides{
|
||||
PrimaryModel: &model,
|
||||
WorkDir: &workDir,
|
||||
ModulesCSV: &modules,
|
||||
}
|
||||
|
||||
if err := cfg.ApplyCLIOverrides(overrides); err != nil {
|
||||
t.Fatalf("ApplyCLIOverrides failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg.PrimaryLLM.Model != "cli-model" {
|
||||
t.Fatalf("expected CLI model override, got %q", cfg.PrimaryLLM.Model)
|
||||
}
|
||||
if cfg.WorkDir != "/cli/work" {
|
||||
t.Fatalf("expected CLI work dir override, got %q", cfg.WorkDir)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.Modules, []string{"grammar"}) {
|
||||
t.Fatalf("unexpected modules: %#v", cfg.Modules)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationFailures(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.PrimaryLLM.TimeoutSeconds = -1
|
||||
cfg.ValidationMaxPromptTokens = 0
|
||||
cfg.MaxSectionTokens = 100
|
||||
cfg.MinSectionTokens = 200
|
||||
cfg.Thresholds.Grammar = 1.5
|
||||
cfg.WorkDirRetention = WorkDirRetention("sometimes")
|
||||
|
||||
err := cfg.Validate()
|
||||
if err == nil {
|
||||
t.Fatalf("expected validation error")
|
||||
}
|
||||
|
||||
message := err.Error()
|
||||
for _, expected := range []string{
|
||||
"primary llm timeout seconds",
|
||||
"validation max prompt tokens",
|
||||
"min section tokens",
|
||||
"grammar confidence threshold",
|
||||
"work dir retention",
|
||||
} {
|
||||
if !strings.Contains(message, expected) {
|
||||
t.Fatalf("expected error to contain %q, got %q", expected, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveValidationLLMInheritance(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.PrimaryLLM.APIKey = "primary-key"
|
||||
cfg.PrimaryLLM.Model = "primary-model"
|
||||
cfg.PrimaryLLM.BaseURL = "https://primary.example/v1"
|
||||
cfg.PrimaryLLM.TimeoutSeconds = 111
|
||||
cfg.PrimaryLLM.MaxRetries = 2
|
||||
|
||||
effective := cfg.EffectiveValidationLLMConfig()
|
||||
if effective.APIKey != "primary-key" || effective.Model != "primary-model" || effective.BaseURL != "https://primary.example/v1" || effective.TimeoutSeconds != 111 || effective.MaxRetries != 2 {
|
||||
t.Fatalf("unexpected inherited config: %#v", effective)
|
||||
}
|
||||
|
||||
validationTimeout := 222
|
||||
validationRetries := 9
|
||||
cfg.ValidationLLM.APIKey = "validation-key"
|
||||
cfg.ValidationLLM.Model = "validation-model"
|
||||
cfg.ValidationLLM.BaseURL = "https://validation.example/v1"
|
||||
cfg.ValidationLLM.TimeoutSeconds = &validationTimeout
|
||||
cfg.ValidationLLM.MaxRetries = &validationRetries
|
||||
|
||||
effective = cfg.EffectiveValidationLLMConfig()
|
||||
if effective.APIKey != "validation-key" || effective.Model != "validation-model" || effective.BaseURL != "https://validation.example/v1" || effective.TimeoutSeconds != 222 || effective.MaxRetries != 9 {
|
||||
t.Fatalf("unexpected overridden validation config: %#v", effective)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactedConfig(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.PrimaryLLM.APIKey = "secret-primary"
|
||||
cfg.ValidationLLM.APIKey = "secret-validation"
|
||||
|
||||
redacted := cfg.Redacted()
|
||||
|
||||
if redacted.PrimaryLLM.APIKey != redactedSecret {
|
||||
t.Fatalf("expected primary api key to be redacted, got %q", redacted.PrimaryLLM.APIKey)
|
||||
}
|
||||
if redacted.ValidationLLM.APIKey != redactedSecret {
|
||||
t.Fatalf("expected validation api key to be redacted, got %q", redacted.ValidationLLM.APIKey)
|
||||
}
|
||||
if cfg.PrimaryLLM.APIKey != "secret-primary" {
|
||||
t.Fatalf("redaction should not mutate original config")
|
||||
}
|
||||
}
|
||||
|
||||
func mapLookup(values map[string]string) func(string) (string, bool) {
|
||||
return func(key string) (string, bool) {
|
||||
value, ok := values[key]
|
||||
return value, ok
|
||||
}
|
||||
}
|
||||
198
internal/core/config/env.go
Normal file
198
internal/core/config/env.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func LoadFromEnv() (Config, error) {
|
||||
return loadFromLookup(os.LookupEnv)
|
||||
}
|
||||
|
||||
func loadFromLookup(lookup func(string) (string, bool)) (Config, error) {
|
||||
cfg := Default()
|
||||
|
||||
if raw, ok := lookup("AUDITA_MODULES"); ok {
|
||||
modules, err := ParseModulesCSV(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_MODULES: %w", err)
|
||||
}
|
||||
cfg.Modules = modules
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_LLM_API_KEY"); ok {
|
||||
cfg.PrimaryLLM.APIKey = raw
|
||||
} else if raw, ok := lookup("OPENROUTER_API_KEY"); ok {
|
||||
cfg.PrimaryLLM.APIKey = raw
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_VALIDATION_LLM_API_KEY"); ok {
|
||||
cfg.ValidationLLM.APIKey = raw
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_MODEL"); ok {
|
||||
cfg.PrimaryLLM.Model = raw
|
||||
}
|
||||
if raw, ok := lookup("AUDITA_VALIDATION_MODEL"); ok {
|
||||
cfg.ValidationLLM.Model = raw
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_BASE_URL"); ok {
|
||||
cfg.PrimaryLLM.BaseURL = raw
|
||||
}
|
||||
if raw, ok := lookup("AUDITA_VALIDATION_BASE_URL"); ok {
|
||||
cfg.ValidationLLM.BaseURL = raw
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_LLM_TIMEOUT_SECONDS"); ok {
|
||||
value, err := parseInt(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_LLM_TIMEOUT_SECONDS: %w", err)
|
||||
}
|
||||
cfg.PrimaryLLM.TimeoutSeconds = value
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS"); ok {
|
||||
value, err := parseInt(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS: %w", err)
|
||||
}
|
||||
cfg.ValidationLLM.TimeoutSeconds = &value
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_MAX_RETRIES"); ok {
|
||||
value, err := parseInt(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_MAX_RETRIES: %w", err)
|
||||
}
|
||||
cfg.PrimaryLLM.MaxRetries = value
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_VALIDATION_MAX_RETRIES"); ok {
|
||||
value, err := parseInt(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_VALIDATION_MAX_RETRIES: %w", err)
|
||||
}
|
||||
cfg.ValidationLLM.MaxRetries = &value
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_VALIDATION_MAX_PROMPT_TOKENS"); ok {
|
||||
value, err := parseInt(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_VALIDATION_MAX_PROMPT_TOKENS: %w", err)
|
||||
}
|
||||
cfg.ValidationMaxPromptTokens = value
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_MAX_SECTION_TOKENS"); ok {
|
||||
value, err := parseInt(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_MAX_SECTION_TOKENS: %w", err)
|
||||
}
|
||||
cfg.MaxSectionTokens = value
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_MIN_SECTION_TOKENS"); ok {
|
||||
value, err := parseInt(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_MIN_SECTION_TOKENS: %w", err)
|
||||
}
|
||||
cfg.MinSectionTokens = value
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_TARGET_SECTIONS"); ok {
|
||||
value, err := parseInt(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_TARGET_SECTIONS: %w", err)
|
||||
}
|
||||
cfg.TargetSections = &value
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD"); ok {
|
||||
value, err := parseFloat(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD: %w", err)
|
||||
}
|
||||
cfg.Thresholds.Glossary = value
|
||||
}
|
||||
if raw, ok := lookup("AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD"); ok {
|
||||
value, err := parseFloat(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD: %w", err)
|
||||
}
|
||||
cfg.Thresholds.Grammar = value
|
||||
}
|
||||
if raw, ok := lookup("AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD"); ok {
|
||||
value, err := parseFloat(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD: %w", err)
|
||||
}
|
||||
cfg.Thresholds.Homophones = value
|
||||
}
|
||||
if raw, ok := lookup("AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD"); ok {
|
||||
value, err := parseFloat(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD: %w", err)
|
||||
}
|
||||
cfg.Thresholds.SpokenWord = value
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_NORMALIZE_MAX_SEGMENT_GAP"); ok {
|
||||
value, err := parseFloat(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_NORMALIZE_MAX_SEGMENT_GAP: %w", err)
|
||||
}
|
||||
cfg.Normalization.MaxSegmentGap = value
|
||||
}
|
||||
if raw, ok := lookup("AUDITA_NORMALIZE_ELLIPSIS_GAP"); ok {
|
||||
value, err := parseFloat(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_NORMALIZE_ELLIPSIS_GAP: %w", err)
|
||||
}
|
||||
cfg.Normalization.EllipsisGap = value
|
||||
}
|
||||
if raw, ok := lookup("AUDITA_NORMALIZE_MAX_SEGMENT_DURATION"); ok {
|
||||
value, err := parseFloat(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_NORMALIZE_MAX_SEGMENT_DURATION: %w", err)
|
||||
}
|
||||
cfg.Normalization.MaxSegmentDuration = value
|
||||
}
|
||||
if raw, ok := lookup("AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS"); ok {
|
||||
value, err := parseInt(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS: %w", err)
|
||||
}
|
||||
cfg.Normalization.MaxSegmentTokens = value
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_WORK_DIR"); ok {
|
||||
cfg.WorkDir = raw
|
||||
}
|
||||
if raw, ok := lookup("AUDITA_WORK_DIR_RETENTION"); ok {
|
||||
cfg.WorkDirRetention = WorkDirRetention(raw)
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func parseInt(raw string) (int, error) {
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("must be an integer")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func parseFloat(raw string) (float64, error) {
|
||||
value, err := strconv.ParseFloat(raw, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("must be a number")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
119
internal/core/config/flags.go
Normal file
119
internal/core/config/flags.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
type CLIOverrides struct {
|
||||
ModulesCSV *string
|
||||
PrimaryLLMAPIKey *string
|
||||
ValidationLLMAPIKey *string
|
||||
PrimaryModel *string
|
||||
ValidationModel *string
|
||||
PrimaryBaseURL *string
|
||||
ValidationBaseURL *string
|
||||
PrimaryLLMTimeoutSeconds *int
|
||||
ValidationLLMTimeoutSeconds *int
|
||||
MaxRetries *int
|
||||
ValidationMaxRetries *int
|
||||
ValidationMaxPromptTokens *int
|
||||
MaxSectionTokens *int
|
||||
MinSectionTokens *int
|
||||
TargetSections *int
|
||||
GlossaryConfidenceThreshold *float64
|
||||
GrammarConfidenceThreshold *float64
|
||||
HomophonesConfidenceThreshold *float64
|
||||
SpokenWordConfidenceThreshold *float64
|
||||
NormalizeMaxSegmentGap *float64
|
||||
NormalizeEllipsisGap *float64
|
||||
NormalizeMaxSegmentDuration *float64
|
||||
NormalizeMaxSegmentTokens *int
|
||||
WorkDir *string
|
||||
WorkDirRetention *string
|
||||
}
|
||||
|
||||
func (c *Config) ApplyCLIOverrides(overrides CLIOverrides) error {
|
||||
if overrides.ModulesCSV != nil {
|
||||
modules, err := ParseModulesCSV(*overrides.ModulesCSV)
|
||||
if err != nil {
|
||||
return fmt.Errorf("--modules: %w", err)
|
||||
}
|
||||
c.Modules = modules
|
||||
}
|
||||
|
||||
if overrides.PrimaryLLMAPIKey != nil {
|
||||
c.PrimaryLLM.APIKey = *overrides.PrimaryLLMAPIKey
|
||||
}
|
||||
if overrides.ValidationLLMAPIKey != nil {
|
||||
c.ValidationLLM.APIKey = *overrides.ValidationLLMAPIKey
|
||||
}
|
||||
if overrides.PrimaryModel != nil {
|
||||
c.PrimaryLLM.Model = *overrides.PrimaryModel
|
||||
}
|
||||
if overrides.ValidationModel != nil {
|
||||
c.ValidationLLM.Model = *overrides.ValidationModel
|
||||
}
|
||||
if overrides.PrimaryBaseURL != nil {
|
||||
c.PrimaryLLM.BaseURL = *overrides.PrimaryBaseURL
|
||||
}
|
||||
if overrides.ValidationBaseURL != nil {
|
||||
c.ValidationLLM.BaseURL = *overrides.ValidationBaseURL
|
||||
}
|
||||
if overrides.PrimaryLLMTimeoutSeconds != nil {
|
||||
c.PrimaryLLM.TimeoutSeconds = *overrides.PrimaryLLMTimeoutSeconds
|
||||
}
|
||||
if overrides.ValidationLLMTimeoutSeconds != nil {
|
||||
value := *overrides.ValidationLLMTimeoutSeconds
|
||||
c.ValidationLLM.TimeoutSeconds = &value
|
||||
}
|
||||
if overrides.MaxRetries != nil {
|
||||
c.PrimaryLLM.MaxRetries = *overrides.MaxRetries
|
||||
}
|
||||
if overrides.ValidationMaxRetries != nil {
|
||||
value := *overrides.ValidationMaxRetries
|
||||
c.ValidationLLM.MaxRetries = &value
|
||||
}
|
||||
if overrides.ValidationMaxPromptTokens != nil {
|
||||
c.ValidationMaxPromptTokens = *overrides.ValidationMaxPromptTokens
|
||||
}
|
||||
if overrides.MaxSectionTokens != nil {
|
||||
c.MaxSectionTokens = *overrides.MaxSectionTokens
|
||||
}
|
||||
if overrides.MinSectionTokens != nil {
|
||||
c.MinSectionTokens = *overrides.MinSectionTokens
|
||||
}
|
||||
if overrides.TargetSections != nil {
|
||||
value := *overrides.TargetSections
|
||||
c.TargetSections = &value
|
||||
}
|
||||
if overrides.GlossaryConfidenceThreshold != nil {
|
||||
c.Thresholds.Glossary = *overrides.GlossaryConfidenceThreshold
|
||||
}
|
||||
if overrides.GrammarConfidenceThreshold != nil {
|
||||
c.Thresholds.Grammar = *overrides.GrammarConfidenceThreshold
|
||||
}
|
||||
if overrides.HomophonesConfidenceThreshold != nil {
|
||||
c.Thresholds.Homophones = *overrides.HomophonesConfidenceThreshold
|
||||
}
|
||||
if overrides.SpokenWordConfidenceThreshold != nil {
|
||||
c.Thresholds.SpokenWord = *overrides.SpokenWordConfidenceThreshold
|
||||
}
|
||||
if overrides.NormalizeMaxSegmentGap != nil {
|
||||
c.Normalization.MaxSegmentGap = *overrides.NormalizeMaxSegmentGap
|
||||
}
|
||||
if overrides.NormalizeEllipsisGap != nil {
|
||||
c.Normalization.EllipsisGap = *overrides.NormalizeEllipsisGap
|
||||
}
|
||||
if overrides.NormalizeMaxSegmentDuration != nil {
|
||||
c.Normalization.MaxSegmentDuration = *overrides.NormalizeMaxSegmentDuration
|
||||
}
|
||||
if overrides.NormalizeMaxSegmentTokens != nil {
|
||||
c.Normalization.MaxSegmentTokens = *overrides.NormalizeMaxSegmentTokens
|
||||
}
|
||||
if overrides.WorkDir != nil {
|
||||
c.WorkDir = *overrides.WorkDir
|
||||
}
|
||||
if overrides.WorkDirRetention != nil {
|
||||
c.WorkDirRetention = WorkDirRetention(*overrides.WorkDirRetention)
|
||||
}
|
||||
|
||||
return c.Validate()
|
||||
}
|
||||
17
internal/core/config/redaction.go
Normal file
17
internal/core/config/redaction.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package config
|
||||
|
||||
const redactedSecret = "[REDACTED]"
|
||||
|
||||
func (c Config) Redacted() Config {
|
||||
redacted := c
|
||||
redacted.PrimaryLLM.APIKey = redactSecret(redacted.PrimaryLLM.APIKey)
|
||||
redacted.ValidationLLM.APIKey = redactSecret(redacted.ValidationLLM.APIKey)
|
||||
return redacted
|
||||
}
|
||||
|
||||
func redactSecret(value string) string {
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
return redactedSecret
|
||||
}
|
||||
106
internal/core/config/validation.go
Normal file
106
internal/core/config/validation.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c Config) Validate() error {
|
||||
var issues []string
|
||||
|
||||
if len(c.Modules) == 0 {
|
||||
issues = append(issues, "modules must not be empty")
|
||||
}
|
||||
for _, module := range c.Modules {
|
||||
if strings.TrimSpace(module) == "" {
|
||||
issues = append(issues, "modules must not contain empty values")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if c.PrimaryLLM.TimeoutSeconds <= 0 {
|
||||
issues = append(issues, "primary llm timeout seconds must be greater than zero")
|
||||
}
|
||||
if c.PrimaryLLM.MaxRetries < 0 {
|
||||
issues = append(issues, "max retries must be zero or greater")
|
||||
}
|
||||
|
||||
if c.ValidationLLM.TimeoutSeconds != nil && *c.ValidationLLM.TimeoutSeconds <= 0 {
|
||||
issues = append(issues, "validation llm timeout seconds must be greater than zero")
|
||||
}
|
||||
if c.ValidationLLM.MaxRetries != nil && *c.ValidationLLM.MaxRetries < 0 {
|
||||
issues = append(issues, "validation max retries must be zero or greater")
|
||||
}
|
||||
|
||||
if c.ValidationMaxPromptTokens <= 0 {
|
||||
issues = append(issues, "validation max prompt tokens must be greater than zero")
|
||||
}
|
||||
if c.MaxSectionTokens <= 0 {
|
||||
issues = append(issues, "max section tokens must be greater than zero")
|
||||
}
|
||||
if c.MinSectionTokens <= 0 {
|
||||
issues = append(issues, "min section tokens must be greater than zero")
|
||||
}
|
||||
if c.MinSectionTokens > c.MaxSectionTokens {
|
||||
issues = append(issues, "min section tokens must be less than or equal to max section tokens")
|
||||
}
|
||||
if c.TargetSections != nil && *c.TargetSections <= 0 {
|
||||
issues = append(issues, "target sections must be greater than zero when set")
|
||||
}
|
||||
|
||||
if err := validateConfidence("glossary", c.Thresholds.Glossary); err != nil {
|
||||
issues = append(issues, err.Error())
|
||||
}
|
||||
if err := validateConfidence("grammar", c.Thresholds.Grammar); err != nil {
|
||||
issues = append(issues, err.Error())
|
||||
}
|
||||
if err := validateConfidence("homophones", c.Thresholds.Homophones); err != nil {
|
||||
issues = append(issues, err.Error())
|
||||
}
|
||||
if err := validateConfidence("spoken-word", c.Thresholds.SpokenWord); err != nil {
|
||||
issues = append(issues, err.Error())
|
||||
}
|
||||
|
||||
if c.Normalization.MaxSegmentGap < 0 {
|
||||
issues = append(issues, "normalize max segment gap must be zero or greater")
|
||||
}
|
||||
if c.Normalization.EllipsisGap < 0 {
|
||||
issues = append(issues, "normalize ellipsis gap must be zero or greater")
|
||||
}
|
||||
if c.Normalization.MaxSegmentDuration <= 0 {
|
||||
issues = append(issues, "normalize max segment duration must be greater than zero")
|
||||
}
|
||||
if c.Normalization.MaxSegmentTokens <= 0 {
|
||||
issues = append(issues, "normalize max segment tokens must be greater than zero")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(c.WorkDir) == "" {
|
||||
issues = append(issues, "work dir must not be empty")
|
||||
}
|
||||
|
||||
if err := validateRetention(c.WorkDirRetention); err != nil {
|
||||
issues = append(issues, err.Error())
|
||||
}
|
||||
|
||||
if len(issues) > 0 {
|
||||
return fmt.Errorf("invalid config: %s", strings.Join(issues, "; "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateConfidence(name string, threshold float64) error {
|
||||
if threshold < 0.0 || threshold > 1.0 {
|
||||
return fmt.Errorf("%s confidence threshold must be between 0.0 and 1.0", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRetention(retention WorkDirRetention) error {
|
||||
switch retention {
|
||||
case WorkDirRetentionAuto, WorkDirRetentionAlways, WorkDirRetentionNever:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("work dir retention must be one of: auto, always, never")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user