package cli import ( "errors" "flag" "fmt" "io" "strings" "gitea.maximumdirect.net/eric/audita/internal/core/config" ) const processNotImplementedMessage = "process command is not implemented yet" // Run executes the Audita CLI with the provided arguments and streams. func Run(args []string, stdout, stderr io.Writer) int { if len(args) == 0 { writeRootUsage(stdout) return 0 } if isHelpCommand(args) { writeRootUsage(stdout) return 0 } if args[0] == "process" { return runProcess(args[1:], stdout, stderr) } fmt.Fprintf(stderr, "audita: unknown command %q\n\n", args[0]) writeRootUsage(stderr) return 2 } func runProcess(args []string, stdout, stderr io.Writer) int { if isHelpCommand(args) || hasHelpFlag(args) { writeProcessUsage(stdout) 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", 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 _ = 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) { writeProcessUsage(stdout) return 0 } 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") fmt.Fprintln(stderr) writeProcessUsage(stderr) return 2 } fmt.Fprintf(stderr, "audita process: %s\n", processNotImplementedMessage) return 1 } func isHelpCommand(args []string) bool { if len(args) == 0 { return false } if len(args) == 1 { switch args[0] { case "help", "-h", "--help": return true } } if len(args) == 2 && args[0] == "help" { switch args[1] { case "process": return true } } return false } func hasHelpFlag(args []string) bool { for _, arg := range args { if arg == "-h" || arg == "--help" { return true } } return false } func writeRootUsage(w io.Writer) { fmt.Fprintln(w, "Audita is a transcript polishing CLI.") fmt.Fprintln(w) fmt.Fprintln(w, "Usage:") fmt.Fprintln(w, " audita [options]") fmt.Fprintln(w) fmt.Fprintln(w, "Commands:") fmt.Fprintln(w, " process Process a transcript JSON file") fmt.Fprintln(w) fmt.Fprintln(w, "Example:") fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json") } func writeProcessUsage(w io.Writer) { fmt.Fprintln(w, "Process a transcript JSON file.") fmt.Fprintln(w) fmt.Fprintln(w, "Usage:") fmt.Fprintln(w, " audita process [flags]") fmt.Fprintln(w) fmt.Fprintln(w, "Flags:") fmt.Fprintln(w, " --glossary Path to glossary YAML file") fmt.Fprintln(w, " --output Path to corrected transcript JSON output file") fmt.Fprintln(w, " --report-json Path to machine-readable report JSON output file") fmt.Fprintln(w, " --modules Comma-separated module sequence override") fmt.Fprintln(w, " --llm-api-key Primary LLM API key") fmt.Fprintln(w, " --model Primary LLM model") fmt.Fprintln(w, " --base-url Primary OpenAI-compatible API base URL") fmt.Fprintln(w, " --work-dir Per-run work directory") fmt.Fprintln(w, " --work-dir-retention auto|always|never") fmt.Fprintln(w) fmt.Fprintln(w, "Example:") fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json") }