diff --git a/README.md b/README.md index 3bdb122..4b42fc5 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,24 @@ Seriatim config contract in `pipeline.yml`: - allowed `seriatim.output_schema` values: `seriatim-minimal`, `seriatim-intermediate`, `seriatim-full` - optional tuning: `seriatim.env.*` (`overlap_word_run_gap`, `overlap_word_run_reorder_window`, `backchannel_max_duration`, `filler_max_duration`) must be `> 0` when provided +Audita config contract in `pipeline.yml`: + +- required: `audita.binary` (name or path; existence is checked at execution time, not config validation time) +- required: `audita.llm_api_key_env` (environment variable name holding the API key secret) +- defaulted when omitted: `audita.timeout` (`3h`), `audita.llm_api_key_env` (`AUDITA_LLM_API_KEY`), `audita.modules` (`glossary,homophones,glossary,spoken_word,grammar,homophones,glossary`), `audita.base_url` (`https://openrouter.ai/api/v1`), `audita.model` (`openrouter/google/gemma-4-31b-it`), `audita.llm_concurrency` (`1`), `audita.validation_model` (`""`), `audita.validation_llm_concurrency` (`1`), `audita.report` (`true`) +- allowed `audita.modules` values: `glossary`, `homophones`, `spoken_word`, `grammar` (order and repeats are allowed) +- `audita.base_url` must be a valid URL when provided +- `audita.llm_concurrency` and `audita.validation_llm_concurrency` must be `> 0` + +Audita credentials note: + +- store only the environment variable **name** in config (`audita.llm_api_key_env`), never the API key value itself +- API key values must not be written to pipeline config, generated configs, logs, or manifest metadata + +Audita CLI compatibility note: + +- Narratio models `audita.llm_concurrency` in config now, but real adapter wiring should verify whether Audita expects a direct CLI flag or env-based configuration for primary LLM concurrency before implementation. + `speakers.yml` note: - use Seriatim’s documented `match:` format (not the legacy direct mapping style used by older scripts/scaffolds) diff --git a/examples/pipeline.minimal.yml b/examples/pipeline.minimal.yml index 6a10e88..9180751 100644 --- a/examples/pipeline.minimal.yml +++ b/examples/pipeline.minimal.yml @@ -25,7 +25,23 @@ seriatim: filler_max_duration: 1.25 audita: - timeout: 1h + binary: "audita" + timeout: "3h" + llm_api_key_env: "AUDITA_LLM_API_KEY" + modules: + - glossary + - homophones + - glossary + - spoken_word + - grammar + - homophones + - glossary + base_url: "https://openrouter.ai/api/v1" + model: "openrouter/google/gemma-4-31b-it" + llm_concurrency: 1 + validation_model: "" + validation_llm_concurrency: 1 + report: true analyzer: timeout: 20m diff --git a/internal/app/commands_test.go b/internal/app/commands_test.go index 511d950..ba9de53 100644 --- a/internal/app/commands_test.go +++ b/internal/app/commands_test.go @@ -216,7 +216,7 @@ seriatim: coalesce_gap: 3.0 report: true audita: - timeout: 1h + binary: audita analyzer: timeout: 20m artifacts: diff --git a/internal/config/config.go b/internal/config/config.go index 8901e50..bac2821 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -70,9 +70,16 @@ type SeriatimEnvConfig struct { // AuditaConfig configures audita adapter settings. type AuditaConfig struct { - BinaryPath string `yaml:"binary_path"` - Timeout string `yaml:"timeout"` - Args []string `yaml:"args"` + Binary string `yaml:"binary"` + Timeout string `yaml:"timeout"` + LLMAPIKeyEnv string `yaml:"llm_api_key_env"` + Modules []string `yaml:"modules"` + BaseURL string `yaml:"base_url"` + Model string `yaml:"model"` + LLMConcurrency *int `yaml:"llm_concurrency"` + ValidationModel string `yaml:"validation_model"` + ValidationLLMConcurrency *int `yaml:"validation_llm_concurrency"` + Report *bool `yaml:"report"` } // AnalyzerConfig configures analyzer adapter settings. diff --git a/internal/config/load.go b/internal/config/load.go index a132dad..5ba5cfc 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -83,6 +83,7 @@ func applyPipelineDefaults(cfg *PipelineConfig) { } applyWhisperXDefaults(&cfg.WhisperX) applySeriatimDefaults(&cfg.Seriatim) + applyAuditaDefaults(&cfg.Audita) } func applyWhisperXDefaults(cfg *WhisperXConfig) { @@ -129,6 +130,44 @@ func applySeriatimDefaults(cfg *SeriatimConfig) { } } +func applyAuditaDefaults(cfg *AuditaConfig) { + if cfg == nil { + return + } + if cfg.Timeout == "" { + cfg.Timeout = "3h" + } + if cfg.LLMAPIKeyEnv == "" { + cfg.LLMAPIKeyEnv = "AUDITA_LLM_API_KEY" + } + if cfg.Modules == nil { + cfg.Modules = []string{ + "glossary", + "homophones", + "glossary", + "spoken_word", + "grammar", + "homophones", + "glossary", + } + } + if cfg.BaseURL == "" { + cfg.BaseURL = "https://openrouter.ai/api/v1" + } + if cfg.Model == "" { + cfg.Model = "openrouter/google/gemma-4-31b-it" + } + if cfg.LLMConcurrency == nil { + cfg.LLMConcurrency = intPtr(1) + } + if cfg.ValidationLLMConcurrency == nil { + cfg.ValidationLLMConcurrency = intPtr(1) + } + if cfg.Report == nil { + cfg.Report = boolPtr(true) + } +} + func float64Ptr(v float64) *float64 { p := v return &p diff --git a/internal/config/load_validate_test.go b/internal/config/load_validate_test.go index 46bd4f7..a15ffa7 100644 --- a/internal/config/load_validate_test.go +++ b/internal/config/load_validate_test.go @@ -25,7 +25,7 @@ whisperx: seriatim: binary: seriatim audita: - timeout: 1h + binary: audita analyzer: timeout: 20m notification: @@ -350,6 +350,219 @@ inputs: `, wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.seriatim.env.overlap_word_run_gap must be > 0 when provided", }, + { + name: "unknown audita field fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + bogus: true +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantLoadErr: "strict decode failed", + }, + { + name: "missing audita binary fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + timeout: 3h +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.binary is required", + }, + { + name: "invalid audita timeout fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + timeout: bad-timeout +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.timeout must be a valid duration", + }, + { + name: "empty audita llm_api_key_env fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + llm_api_key_env: " " +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.llm_api_key_env is required", + }, + { + name: "empty audita modules fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + modules: [] +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.modules must include at least one module", + }, + { + name: "empty audita module item fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + modules: + - glossary + - "" +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.modules[1] must be non-empty", + }, + { + name: "invalid audita module item fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + modules: + - glossary + - bad_module +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.modules[1] must be one of: glossary, homophones, spoken_word, grammar", + }, + { + name: "invalid audita base_url fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + base_url: :// +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.base_url must be a valid URL", + }, + { + name: "invalid audita llm_concurrency fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + llm_concurrency: 0 +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.llm_concurrency must be > 0", + }, + { + name: "invalid audita validation_llm_concurrency fails", + pipelineYAML: `workspace: + root: /tmp/narratio +whisperx: + transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe +seriatim: + binary: seriatim +audita: + binary: audita + validation_llm_concurrency: 0 +`, + sessionYAML: `session_id: 2026-05-03 +inputs: + audio_dir: ./audio + speakers_file: ./speakers.yml + autocorrect_file: ./autocorrect.yml + glossary_file: ./glossary.yml +`, + wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.validation_llm_concurrency must be > 0", + }, } for _, tt := range tests { @@ -407,6 +620,33 @@ inputs: if cfg.Pipeline.Seriatim.Report == nil || *cfg.Pipeline.Seriatim.Report != true { t.Fatalf("seriatim.report = %v, want true", cfg.Pipeline.Seriatim.Report) } + if cfg.Pipeline.Audita.Timeout != "3h" { + t.Fatalf("audita.timeout = %q, want %q", cfg.Pipeline.Audita.Timeout, "3h") + } + if cfg.Pipeline.Audita.LLMAPIKeyEnv != "AUDITA_LLM_API_KEY" { + t.Fatalf("audita.llm_api_key_env = %q, want %q", cfg.Pipeline.Audita.LLMAPIKeyEnv, "AUDITA_LLM_API_KEY") + } + if got := strings.Join(cfg.Pipeline.Audita.Modules, ","); got != "glossary,homophones,glossary,spoken_word,grammar,homophones,glossary" { + t.Fatalf("audita.modules = %q, want default sequence", got) + } + if cfg.Pipeline.Audita.BaseURL != "https://openrouter.ai/api/v1" { + t.Fatalf("audita.base_url = %q, want %q", cfg.Pipeline.Audita.BaseURL, "https://openrouter.ai/api/v1") + } + if cfg.Pipeline.Audita.Model != "openrouter/google/gemma-4-31b-it" { + t.Fatalf("audita.model = %q, want %q", cfg.Pipeline.Audita.Model, "openrouter/google/gemma-4-31b-it") + } + if cfg.Pipeline.Audita.LLMConcurrency == nil || *cfg.Pipeline.Audita.LLMConcurrency != 1 { + t.Fatalf("audita.llm_concurrency = %v, want 1", cfg.Pipeline.Audita.LLMConcurrency) + } + if cfg.Pipeline.Audita.ValidationModel != "" { + t.Fatalf("audita.validation_model = %q, want empty default", cfg.Pipeline.Audita.ValidationModel) + } + if cfg.Pipeline.Audita.ValidationLLMConcurrency == nil || *cfg.Pipeline.Audita.ValidationLLMConcurrency != 1 { + t.Fatalf("audita.validation_llm_concurrency = %v, want 1", cfg.Pipeline.Audita.ValidationLLMConcurrency) + } + if cfg.Pipeline.Audita.Report == nil || *cfg.Pipeline.Audita.Report != true { + t.Fatalf("audita.report = %v, want true", cfg.Pipeline.Audita.Report) + } } err = Validate(cfg) @@ -447,6 +687,18 @@ func TestValidateMissingAudioSource(t *testing.T) { CoalesceGap: float64Ptr(3.0), Report: boolPtr(true), }, + Audita: AuditaConfig{ + Binary: "audita", + Timeout: "3h", + LLMAPIKeyEnv: "AUDITA_LLM_API_KEY", + Modules: []string{"glossary", "homophones"}, + BaseURL: "https://openrouter.ai/api/v1", + Model: "openrouter/google/gemma-4-31b-it", + LLMConcurrency: intPtr(1), + ValidationModel: "", + ValidationLLMConcurrency: intPtr(1), + Report: boolPtr(true), + }, }, Session: &SessionConfig{ SessionID: "2026-05-03", @@ -485,6 +737,12 @@ func TestExamplesLoadAndValidate(t *testing.T) { func writeConfigFiles(t *testing.T, pipelineYAML, sessionYAML string) (string, string) { t.Helper() + if !strings.Contains(pipelineYAML, "\naudita:") && !strings.HasPrefix(pipelineYAML, "audita:") { + if !strings.HasSuffix(pipelineYAML, "\n") { + pipelineYAML += "\n" + } + pipelineYAML += "audita:\n binary: audita\n" + } dir := t.TempDir() pipelinePath := filepath.Join(dir, "pipeline.yml") diff --git a/internal/config/validate.go b/internal/config/validate.go index 6e0df67..371b600 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -39,7 +39,7 @@ func validatePipeline(cfg *PipelineConfig) error { if err := validateSeriatim(cfg.Seriatim); err != nil { return err } - if err := validateDuration("pipeline.audita.timeout", cfg.Audita.Timeout); err != nil { + if err := validateAudita(cfg.Audita); err != nil { return err } if err := validateDuration("pipeline.analyzer.timeout", cfg.Analyzer.Timeout); err != nil { @@ -122,6 +122,57 @@ func validateSeriatim(cfg SeriatimConfig) error { return nil } +func validateAudita(cfg AuditaConfig) error { + if strings.TrimSpace(cfg.Binary) == "" { + return fmt.Errorf("pipeline.audita.binary is required") + } + if err := validateDuration("pipeline.audita.timeout", cfg.Timeout); err != nil { + return err + } + if strings.TrimSpace(cfg.LLMAPIKeyEnv) == "" { + return fmt.Errorf("pipeline.audita.llm_api_key_env is required") + } + if len(cfg.Modules) == 0 { + return fmt.Errorf("pipeline.audita.modules must include at least one module") + } + for i, mod := range cfg.Modules { + m := strings.TrimSpace(mod) + if m == "" { + return fmt.Errorf("pipeline.audita.modules[%d] must be non-empty", i) + } + switch m { + case "glossary", "homophones", "spoken_word", "grammar": + default: + return fmt.Errorf("pipeline.audita.modules[%d] must be one of: glossary, homophones, spoken_word, grammar", i) + } + } + if strings.TrimSpace(cfg.BaseURL) != "" { + u, err := url.Parse(cfg.BaseURL) + if err != nil || u.Scheme == "" || u.Host == "" { + if err != nil { + return fmt.Errorf("pipeline.audita.base_url must be a valid URL: %w", err) + } + return fmt.Errorf("pipeline.audita.base_url must be a valid URL") + } + } + if strings.TrimSpace(cfg.Model) == "" { + return fmt.Errorf("pipeline.audita.model is required") + } + if cfg.LLMConcurrency == nil { + return fmt.Errorf("pipeline.audita.llm_concurrency must be set (defaults should populate this)") + } + if *cfg.LLMConcurrency <= 0 { + return fmt.Errorf("pipeline.audita.llm_concurrency must be > 0") + } + if cfg.ValidationLLMConcurrency == nil { + return fmt.Errorf("pipeline.audita.validation_llm_concurrency must be set (defaults should populate this)") + } + if *cfg.ValidationLLMConcurrency <= 0 { + return fmt.Errorf("pipeline.audita.validation_llm_concurrency must be > 0") + } + return nil +} + func validateSession(cfg *SessionConfig) error { if strings.TrimSpace(cfg.SessionID) == "" { return fmt.Errorf("session.session_id is required") diff --git a/reference/audita/README.md b/reference/audita/README.md new file mode 100644 index 0000000..e13d571 --- /dev/null +++ b/reference/audita/README.md @@ -0,0 +1,147 @@ +# Audita + +Audita is a framework-first transcript correction application. The public `audita` package provides: + +- deterministic transcript normalization +- token-batched module orchestration +- concrete `glossary`, `homophones`, `spoken_word`, and `grammar` modules built on reusable proposal / validator contracts +- structured run reporting and work-dir diagnostics + +The previous working implementation has been preserved as `audita_prototype` inside this repository. Its full regression suite lives under `tests/audita_prototype`. + +## Development + +This project is set up for `uv`. + +```sh +uv sync --extra dev +uv run pytest +``` + +## Usage + +Process a transcript with the current framework implementation: + +```sh +uv run audita process transcript.json --glossary glossary.yaml --output corrected.json +``` + +The framework currently runs this default module sequence: + +1. `glossary` +2. `homophones` +3. `glossary` +4. `spoken_word` +5. `grammar` + +Resolved run instance names are auto-numbered for repeats, so the default report pipeline is: + +1. `glossary_1` +2. `homophones` +3. `glossary_2` +4. `spoken_word` +5. `grammar` + +The default module sequence is fully implemented today: + +- `glossary` proposes glossary-supported acoustic corrections +- `homophones` proposes conservative homophone and mistranscription corrections +- `spoken_word` proposes conservative dysfluency cleanup +- `grammar` proposes punctuation, capitalization, and spacing cleanup only + +To run a custom module sequence, pass `--modules`: + +```sh +uv run audita process transcript.json --glossary glossary.yaml --modules grammar --output corrected.json +``` + +To also write a structured JSON report: + +```sh +uv run audita process transcript.json --glossary glossary.yaml --output corrected.json --report-json report.json +``` + +From a checked-out repository, you can also use the root launcher: + +```sh +./audita process transcript.json --glossary glossary.yaml --output corrected.json +``` + +For a system-wide command, install the source tree under `/usr/local/src/audita`, sync dependencies there, and symlink the root launcher into your `PATH`: + +```sh +cd /usr/local/src/audita +uv sync --extra dev +ln -s /usr/local/src/audita/audita /usr/local/bin/audita +audita process transcript.json --glossary glossary.yaml --output corrected.json +``` + +Without `--output`, Audita writes the corrected transcript JSON to stdout and progress logs to stderr. +`--report-json` writes a separate machine-readable run report and never mixes report data into stdout. + +Useful configuration can be supplied by CLI flag or environment variable. CLI flags take precedence over environment variables. Normal runs now require LLM API credentials, because the `glossary`, `homophones`, `spoken_word`, and `grammar` modules make real LLM calls. `AUDITA_LLM_API_KEY` and `--llm-api-key` are the preferred provider-neutral credential surfaces, while `OPENROUTER_API_KEY` remains supported as a backward-compatible fallback. + +| Environment variable | CLI flag | Default | Purpose | +| --- | --- | --- | --- | +| `AUDITA_MODULES` | `--modules` | `glossary,homophones,glossary,spoken_word,grammar` | Comma-separated logical module keys to run; CLI overrides the environment value | +| `AUDITA_LLM_API_KEY` | `--llm-api-key` | unset | Preferred provider-neutral LLM API credential; CLI overrides both environment-key variants | +| `AUDITA_VALIDATION_LLM_API_KEY` | `--validation-llm-api-key` | unset | Validation-phase LLM API credential; defaults to the primary LLM API key | +| `AUDITA_MODEL` | `--model` | `openrouter/google/gemma-4-31b-it` | LLM model name sent to the configured OpenAI-compatible endpoint | +| `AUDITA_VALIDATION_MODEL` | `--validation-model` | unset | Validation-phase LLM model; defaults to `AUDITA_MODEL` | +| `AUDITA_BASE_URL` | `--base-url` | `https://openrouter.ai/api/v1` | OpenAI-compatible API base URL | +| `AUDITA_VALIDATION_BASE_URL` | `--validation-base-url` | unset | Validation-phase OpenAI-compatible API base URL; defaults to `AUDITA_BASE_URL` | +| `AUDITA_LLM_TIMEOUT_SECONDS` | `--llm-timeout-seconds` | `600` | Per-request timeout in seconds for LLM calls to the configured OpenAI-compatible endpoint | +| `AUDITA_VALIDATION_LLM_TIMEOUT_SECONDS` | `--validation-llm-timeout-seconds` | unset | Validation-phase per-request timeout in seconds; defaults to `AUDITA_LLM_TIMEOUT_SECONDS` | +| `AUDITA_VALIDATION_MAX_PROMPT_TOKENS` | `--validation-max-prompt-tokens` | `2048` | Maximum estimated tokens per validation-phase LLM prompt batch | +| `AUDITA_TARGET_SECTIONS` | `--target-sections` | unset | Exact number of contiguous proposal-stage transcript sections; errors if min/max token bounds cannot be satisfied | +| `AUDITA_MAX_RETRIES` | `--max-retries` | `3` | Maximum Instructor retries for structured responses | +| `AUDITA_VALIDATION_MAX_RETRIES` | `--validation-max-retries` | unset | Validation-phase structured-output retries; defaults to `AUDITA_MAX_RETRIES` | +| `AUDITA_VALIDATION_LLM_CONCURRENCY` | `--validation-llm-concurrency` | unset | Validation-phase LLM concurrency; defaults to `AUDITA_LLM_CONCURRENCY` | +| `AUDITA_MAX_SECTION_TOKENS` | `--max-section-tokens` | `8192` | Maximum estimated tokens per proposal-stage transcript section | +| `AUDITA_MIN_SECTION_TOKENS` | `--min-section-tokens` | `2048` | Minimum estimated tokens per proposal-stage transcript section when balancing for concurrency | +| `AUDITA_GLOSSARY_CONFIDENCE_THRESHOLD` | `--glossary-confidence-threshold` | `0.8` | Minimum confidence required for glossary proposals to survive validation | +| `AUDITA_GRAMMAR_CONFIDENCE_THRESHOLD` | `--grammar-confidence-threshold` | `0.8` | Minimum confidence required for grammar proposals to survive validation | +| `AUDITA_HOMOPHONES_CONFIDENCE_THRESHOLD` | `--homophones-confidence-threshold` | `0.8` | Minimum confidence required for homophone proposals to survive validation | +| `AUDITA_SPOKEN_WORD_CONFIDENCE_THRESHOLD` | `--spoken-word-confidence-threshold` | `0.8` | Minimum confidence required for spoken-word proposals to survive validation | +| `AUDITA_NORMALIZE_MAX_SEGMENT_GAP` | `--normalize-max-segment-gap` | `4.0` | Same-speaker gaps eligible for deterministic merging | +| `AUDITA_NORMALIZE_ELLIPSIS_GAP` | `--normalize-ellipsis-gap` | `3.5` | Same-speaker gaps above this value are joined with ` ... ` | +| `AUDITA_NORMALIZE_MAX_SEGMENT_DURATION` | `--normalize-max-segment-duration` | `60.0` | Maximum merged segment duration | +| `AUDITA_NORMALIZE_MAX_SEGMENT_TOKENS` | `--normalize-max-segment-tokens` | `2048` | Maximum merged segment prompt payload size | +| `AUDITA_WORK_DIR` | `--work-dir` | `/tmp/audita` | Per-run scratch diagnostics directory | +| `AUDITA_WORK_DIR_RETENTION` | `--work-dir-retention` | `auto` | Whether to retain the per-run work directory: `auto`, `always`, or `never` | + +Set `AUDITA_MODULES=grammar` to run only the grammar module by default, or override it per command with `--modules`. + +Validation-phase LLM settings inherit from the primary `AUDITA_*` LLM settings by default. Set any of the `AUDITA_VALIDATION_*` values only when you want LLM-backed validators to use a different model, endpoint, credential, timeout, retry budget, or concurrency level. + +OpenRouter remains the default out of the box: + +```sh +export AUDITA_LLM_API_KEY=your-openrouter-key +audita process transcript.json --glossary glossary.yaml --output corrected.json +``` + +You can point Audita at any OpenAI-compatible endpoint by changing `AUDITA_BASE_URL` and, if needed, `AUDITA_MODEL`. For example, a local vLLM server: + +```sh +export AUDITA_LLM_API_KEY=local-dev-key +export AUDITA_BASE_URL=http://localhost:8000/v1 +export AUDITA_MODEL=meta-llama/Llama-3.1-8B-Instruct +audita process transcript.json --glossary glossary.yaml --output corrected.json +``` + +Or the actual OpenAI API: + +```sh +export AUDITA_LLM_API_KEY=your-openai-key +export AUDITA_BASE_URL=https://api.openai.com/v1 +export AUDITA_MODEL=gpt-4.1-mini +audita process transcript.json --glossary glossary.yaml --output corrected.json +``` + +`AUDITA_WORK_DIR` stores per-run diagnostics while processing. Under the default `AUDITA_WORK_DIR_RETENTION=auto`, clean successful runs are removed, while failed runs and successful runs with final skipped corrections are preserved. Use `always` to keep every run directory and `never` to remove successful run directories even when skips remain. +Failed runs always preserve the run directory and include an authoritative `report.json` alongside normalization and prompt/response diagnostics. + +## Prototype Archive + +The archived prototype remains importable as `audita_prototype` and is still covered by its original regression suite. This is intentional: the new `audita` package is a framework-oriented rewrite, not a thin wrapper around the old code.