Add Audita configuration contract
This commit is contained in:
18
README.md
18
README.md
@@ -23,6 +23,24 @@ Seriatim config contract in `pipeline.yml`:
|
|||||||
- allowed `seriatim.output_schema` values: `seriatim-minimal`, `seriatim-intermediate`, `seriatim-full`
|
- 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
|
- 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:
|
`speakers.yml` note:
|
||||||
|
|
||||||
- use Seriatim’s documented `match:` format (not the legacy direct mapping style used by older scripts/scaffolds)
|
- use Seriatim’s documented `match:` format (not the legacy direct mapping style used by older scripts/scaffolds)
|
||||||
|
|||||||
@@ -25,7 +25,23 @@ seriatim:
|
|||||||
filler_max_duration: 1.25
|
filler_max_duration: 1.25
|
||||||
|
|
||||||
audita:
|
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:
|
analyzer:
|
||||||
timeout: 20m
|
timeout: 20m
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ seriatim:
|
|||||||
coalesce_gap: 3.0
|
coalesce_gap: 3.0
|
||||||
report: true
|
report: true
|
||||||
audita:
|
audita:
|
||||||
timeout: 1h
|
binary: audita
|
||||||
analyzer:
|
analyzer:
|
||||||
timeout: 20m
|
timeout: 20m
|
||||||
artifacts:
|
artifacts:
|
||||||
|
|||||||
@@ -70,9 +70,16 @@ type SeriatimEnvConfig struct {
|
|||||||
|
|
||||||
// AuditaConfig configures audita adapter settings.
|
// AuditaConfig configures audita adapter settings.
|
||||||
type AuditaConfig struct {
|
type AuditaConfig struct {
|
||||||
BinaryPath string `yaml:"binary_path"`
|
Binary string `yaml:"binary"`
|
||||||
Timeout string `yaml:"timeout"`
|
Timeout string `yaml:"timeout"`
|
||||||
Args []string `yaml:"args"`
|
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.
|
// AnalyzerConfig configures analyzer adapter settings.
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ func applyPipelineDefaults(cfg *PipelineConfig) {
|
|||||||
}
|
}
|
||||||
applyWhisperXDefaults(&cfg.WhisperX)
|
applyWhisperXDefaults(&cfg.WhisperX)
|
||||||
applySeriatimDefaults(&cfg.Seriatim)
|
applySeriatimDefaults(&cfg.Seriatim)
|
||||||
|
applyAuditaDefaults(&cfg.Audita)
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyWhisperXDefaults(cfg *WhisperXConfig) {
|
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 {
|
func float64Ptr(v float64) *float64 {
|
||||||
p := v
|
p := v
|
||||||
return &p
|
return &p
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ whisperx:
|
|||||||
seriatim:
|
seriatim:
|
||||||
binary: seriatim
|
binary: seriatim
|
||||||
audita:
|
audita:
|
||||||
timeout: 1h
|
binary: audita
|
||||||
analyzer:
|
analyzer:
|
||||||
timeout: 20m
|
timeout: 20m
|
||||||
notification:
|
notification:
|
||||||
@@ -350,6 +350,219 @@ inputs:
|
|||||||
`,
|
`,
|
||||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.seriatim.env.overlap_word_run_gap must be > 0 when provided",
|
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 {
|
for _, tt := range tests {
|
||||||
@@ -407,6 +620,33 @@ inputs:
|
|||||||
if cfg.Pipeline.Seriatim.Report == nil || *cfg.Pipeline.Seriatim.Report != true {
|
if cfg.Pipeline.Seriatim.Report == nil || *cfg.Pipeline.Seriatim.Report != true {
|
||||||
t.Fatalf("seriatim.report = %v, want true", cfg.Pipeline.Seriatim.Report)
|
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)
|
err = Validate(cfg)
|
||||||
@@ -447,6 +687,18 @@ func TestValidateMissingAudioSource(t *testing.T) {
|
|||||||
CoalesceGap: float64Ptr(3.0),
|
CoalesceGap: float64Ptr(3.0),
|
||||||
Report: boolPtr(true),
|
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{
|
Session: &SessionConfig{
|
||||||
SessionID: "2026-05-03",
|
SessionID: "2026-05-03",
|
||||||
@@ -485,6 +737,12 @@ func TestExamplesLoadAndValidate(t *testing.T) {
|
|||||||
|
|
||||||
func writeConfigFiles(t *testing.T, pipelineYAML, sessionYAML string) (string, string) {
|
func writeConfigFiles(t *testing.T, pipelineYAML, sessionYAML string) (string, string) {
|
||||||
t.Helper()
|
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()
|
dir := t.TempDir()
|
||||||
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func validatePipeline(cfg *PipelineConfig) error {
|
|||||||
if err := validateSeriatim(cfg.Seriatim); err != nil {
|
if err := validateSeriatim(cfg.Seriatim); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := validateDuration("pipeline.audita.timeout", cfg.Audita.Timeout); err != nil {
|
if err := validateAudita(cfg.Audita); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := validateDuration("pipeline.analyzer.timeout", cfg.Analyzer.Timeout); err != nil {
|
if err := validateDuration("pipeline.analyzer.timeout", cfg.Analyzer.Timeout); err != nil {
|
||||||
@@ -122,6 +122,57 @@ func validateSeriatim(cfg SeriatimConfig) error {
|
|||||||
return nil
|
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 {
|
func validateSession(cfg *SessionConfig) error {
|
||||||
if strings.TrimSpace(cfg.SessionID) == "" {
|
if strings.TrimSpace(cfg.SessionID) == "" {
|
||||||
return fmt.Errorf("session.session_id is required")
|
return fmt.Errorf("session.session_id is required")
|
||||||
|
|||||||
147
reference/audita/README.md
Normal file
147
reference/audita/README.md
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user