Files
narratio/internal/config/validate.go

161 lines
4.8 KiB
Go

package config
import (
"fmt"
"net/url"
"strings"
"time"
)
// Validate checks resolved configuration for required fields and parseable durations.
func Validate(cfg *Config) error {
if cfg == nil {
return fmt.Errorf("config is nil")
}
if cfg.Pipeline == nil {
return fmt.Errorf("pipeline config is required")
}
if cfg.Session == nil {
return fmt.Errorf("session config is required")
}
if err := validatePipeline(cfg.Pipeline); err != nil {
return fmt.Errorf("pipeline config %q invalid: %w", shortName(cfg.PipelinePath, "pipeline.yml"), err)
}
if err := validateSession(cfg.Session); err != nil {
return fmt.Errorf("session config %q invalid: %w", shortName(cfg.SessionPath, "session.yml"), err)
}
return nil
}
func validatePipeline(cfg *PipelineConfig) error {
if strings.TrimSpace(cfg.Workspace.Root) == "" {
return fmt.Errorf("pipeline.workspace.root is required")
}
if err := validateWhisperX(cfg.WhisperX); err != nil {
return err
}
if err := validateSeriatim(cfg.Seriatim); err != nil {
return err
}
if err := validateDuration("pipeline.audita.timeout", cfg.Audita.Timeout); err != nil {
return err
}
if err := validateDuration("pipeline.analyzer.timeout", cfg.Analyzer.Timeout); err != nil {
return err
}
if err := validateDuration("pipeline.notification.timeout", cfg.Notification.Timeout); err != nil {
return err
}
return nil
}
func validateWhisperX(cfg WhisperXConfig) error {
if strings.TrimSpace(cfg.TranscribeURL) == "" {
return fmt.Errorf("pipeline.whisperx.transcribe_url is required")
}
u, err := url.Parse(cfg.TranscribeURL)
if err != nil || u.Scheme == "" || u.Host == "" {
if err != nil {
return fmt.Errorf("pipeline.whisperx.transcribe_url must be a valid URL: %w", err)
}
return fmt.Errorf("pipeline.whisperx.transcribe_url must be a valid URL")
}
if err := validateDuration("pipeline.whisperx.timeout", cfg.Timeout); err != nil {
return err
}
if err := validateDuration("pipeline.whisperx.retry_delay", cfg.RetryDelay); err != nil {
return err
}
if cfg.Retries == nil {
return fmt.Errorf("pipeline.whisperx.retries must be set (defaults should populate this)")
}
if *cfg.Retries < 0 {
return fmt.Errorf("pipeline.whisperx.retries must be >= 0")
}
if cfg.Concurrency == nil {
return fmt.Errorf("pipeline.whisperx.concurrency must be set (defaults should populate this)")
}
if *cfg.Concurrency <= 0 {
return fmt.Errorf("pipeline.whisperx.concurrency must be > 0")
}
return nil
}
func validateSeriatim(cfg SeriatimConfig) error {
if strings.TrimSpace(cfg.Binary) == "" {
return fmt.Errorf("pipeline.seriatim.binary is required")
}
if err := validateDuration("pipeline.seriatim.timeout", cfg.Timeout); err != nil {
return err
}
switch cfg.OutputSchema {
case "seriatim-minimal", "seriatim-intermediate", "seriatim-full":
default:
return fmt.Errorf("pipeline.seriatim.output_schema must be one of: seriatim-minimal, seriatim-intermediate, seriatim-full")
}
if cfg.CoalesceGap == nil {
return fmt.Errorf("pipeline.seriatim.coalesce_gap must be set (defaults should populate this)")
}
if *cfg.CoalesceGap < 0 {
return fmt.Errorf("pipeline.seriatim.coalesce_gap must be >= 0")
}
for _, item := range []struct {
name string
value *float64
}{
{name: "pipeline.seriatim.env.overlap_word_run_gap", value: cfg.Env.OverlapWordRunGap},
{name: "pipeline.seriatim.env.overlap_word_run_reorder_window", value: cfg.Env.OverlapWordRunReorderWindow},
{name: "pipeline.seriatim.env.backchannel_max_duration", value: cfg.Env.BackchannelMaxDuration},
{name: "pipeline.seriatim.env.filler_max_duration", value: cfg.Env.FillerMaxDuration},
} {
if item.value != nil && *item.value <= 0 {
return fmt.Errorf("%s must be > 0 when provided", item.name)
}
}
return nil
}
func validateSession(cfg *SessionConfig) error {
if strings.TrimSpace(cfg.SessionID) == "" {
return fmt.Errorf("session.session_id is required")
}
if strings.TrimSpace(cfg.Inputs.SpeakersFile) == "" {
return fmt.Errorf("session.inputs.speakers_file is required")
}
if strings.TrimSpace(cfg.Inputs.AutocorrectFile) == "" {
return fmt.Errorf("session.inputs.autocorrect_file is required")
}
if strings.TrimSpace(cfg.Inputs.GlossaryFile) == "" {
return fmt.Errorf("session.inputs.glossary_file is required")
}
hasAudioDir := strings.TrimSpace(cfg.Inputs.AudioDir) != ""
hasAudioFiles := len(cfg.Inputs.AudioFiles) > 0
if !hasAudioDir && !hasAudioFiles {
return fmt.Errorf("session.inputs requires audio_dir or at least one audio_files entry")
}
return nil
}
func validateDuration(fieldName, value string) error {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return nil
}
if _, err := time.ParseDuration(trimmed); err != nil {
return fmt.Errorf("%s must be a valid duration: %w", fieldName, err)
}
return nil
}