Files
audita/internal/cli/run.go

262 lines
9.9 KiB
Go

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 <command> [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 <transcript.json> [flags]")
fmt.Fprintln(w)
fmt.Fprintln(w, "Flags:")
fmt.Fprintln(w, " --glossary <path> Path to glossary YAML file")
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")
}