Simplify the CLI interface and update documentation accordingly

This commit is contained in:
2026-04-26 19:47:47 -05:00
parent 3928e0c4a7
commit f9ca80f2e8
5 changed files with 92 additions and 62 deletions

View File

@@ -58,6 +58,11 @@ func (autocorrectPostprocessor) Process(ctx context.Context, in model.MergedTran
if err := ctx.Err(); err != nil {
return model.MergedTranscript{}, nil, err
}
if cfg.AutocorrectFile == "" {
return in, []report.Event{
report.Info("postprocessing", "autocorrect", "skipped autocorrect because no autocorrect file was supplied"),
}, nil
}
rules, err := autocorrect.Load(cfg.AutocorrectFile)
if err != nil {

View File

@@ -3,6 +3,7 @@ package builtin
import (
"context"
"fmt"
"path/filepath"
"strings"
"gitea.maximumdirect.net/eric/seriatim/internal/config"
@@ -99,16 +100,25 @@ func (normalizeSpeakers) Process(ctx context.Context, in pipeline.PreprocessStat
return pipeline.PreprocessState{}, nil, fmt.Errorf("preprocessing module %q requires state %q but received %q", "normalize-speakers", pipeline.StateRaw, in.State)
}
speakers, err := speaker.LoadMap(cfg.SpeakersFile)
if err != nil {
return pipeline.PreprocessState{}, nil, err
var speakers speaker.Map
useSpeakerMap := cfg.SpeakersFile != ""
if useSpeakerMap {
var err error
speakers, err = speaker.LoadMap(cfg.SpeakersFile)
if err != nil {
return pipeline.PreprocessState{}, nil, err
}
}
canonical := make([]model.CanonicalTranscript, 0, len(in.Raw))
for _, raw := range in.Raw {
canonicalSpeaker, err := speakers.SpeakerForSource(raw.Source)
if err != nil {
return pipeline.PreprocessState{}, nil, err
canonicalSpeaker := filepath.Base(raw.Source)
if useSpeakerMap {
var err error
canonicalSpeaker, err = speakers.SpeakerForSource(raw.Source)
if err != nil {
return pipeline.PreprocessState{}, nil, err
}
}
segments := make([]model.Segment, 0, len(raw.Segments))
@@ -129,11 +139,16 @@ func (normalizeSpeakers) Process(ctx context.Context, in pipeline.PreprocessStat
})
}
message := "created canonical transcript(s) from raw input"
if !useSpeakerMap {
message = "created canonical transcript(s) using input basenames as speaker labels"
}
return pipeline.PreprocessState{
State: pipeline.StateCanonical,
Raw: append([]model.RawTranscript(nil), in.Raw...),
Canonical: canonical,
}, []report.Event{
report.Info("preprocessing", "normalize-speakers", "created canonical transcript(s) from raw input"),
report.Info("preprocessing", "normalize-speakers", message),
}, nil
}

View File

@@ -90,6 +90,7 @@ func TestMergeWritesMergedOutputAndReport(t *testing.T) {
"placeholder-merger",
"detect-overlaps",
"resolve-overlaps",
"autocorrect",
"assign-ids",
"validate-output",
"json",
@@ -274,43 +275,37 @@ func TestMissingInputFileFailsBeforePipelineExecution(t *testing.T) {
}
}
func TestNormalizeSpeakersRequiresSpeakersFile(t *testing.T) {
func TestDefaultMergeWorksWithoutSpeakersOrAutocorrect(t *testing.T) {
dir := t.TempDir()
input := writeJSONFile(t, dir, "input.json", `{"segments":[]}`)
input := writeJSONFile(t, dir, "input.json", `{"segments":[{"start":1,"end":2,"text":"Frank"}]}`)
output := filepath.Join(dir, "merged.json")
reportPath := filepath.Join(dir, "report.json")
err := executeMerge(
"--input-file", input,
"--output-file", output,
"--report-file", reportPath,
)
if err == nil {
t.Fatal("expected error")
if err != nil {
t.Fatalf("merge failed: %v", err)
}
if !strings.Contains(err.Error(), "--speakers is required") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestAutocorrectRequiresAutocorrectFile(t *testing.T) {
dir := t.TempDir()
input := writeJSONFile(t, dir, "input.json", `{"segments":[]}`)
speakers := writeYAMLFile(t, dir, "speakers.yml", `match:
- speaker: Alice
match: ["input.json"]
`)
output := filepath.Join(dir, "merged.json")
err := executeMerge(
"--input-file", input,
"--speakers", speakers,
"--output-file", output,
"--postprocessing-modules", "detect-overlaps,resolve-overlaps,autocorrect,assign-ids,validate-output",
)
if err == nil {
t.Fatal("expected error")
var transcript model.FinalTranscript
readJSON(t, output, &transcript)
if got, want := transcript.Segments[0].Speaker, "input.json"; got != want {
t.Fatalf("speaker = %q, want %q", got, want)
}
if !strings.Contains(err.Error(), "--autocorrect is required") {
t.Fatalf("unexpected error: %v", err)
if got, want := transcript.Segments[0].Text, "Frank"; got != want {
t.Fatalf("text = %q, want %q", got, want)
}
var rpt report.Report
readJSON(t, reportPath, &rpt)
if !hasReportEvent(rpt, "preprocessing", "normalize-speakers", "using input basenames") {
t.Fatal("expected normalize-speakers fallback report event")
}
if !hasReportEvent(rpt, "postprocessing", "autocorrect", "skipped autocorrect") {
t.Fatal("expected autocorrect skip report event")
}
}
@@ -402,6 +397,28 @@ func TestPostprocessingAutocorrectUpdatesOutputAndReport(t *testing.T) {
}
}
func TestInvalidAutocorrectFileFailsWhenProvided(t *testing.T) {
dir := t.TempDir()
input := writeJSONFile(t, dir, "input.json", `{"segments":[{"start":1,"end":2,"text":"Frank"}]}`)
output := filepath.Join(dir, "merged.json")
autocorrect := writeYAMLFile(t, dir, "autocorrect.yml", `autocorrect:
- target: ""
match: ["Frank"]
`)
err := executeMerge(
"--input-file", input,
"--autocorrect", autocorrect,
"--output-file", output,
)
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "must include target") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestOutputJSONIsByteStable(t *testing.T) {
dir := t.TempDir()
inputA := writeJSONFile(t, dir, "a.json", `{"segments":[{"start":2,"end":3,"text":"a"}]}`)
@@ -658,6 +675,15 @@ func equalStrings(left []string, right []string) bool {
return true
}
func hasReportEvent(rpt report.Report, stage string, module string, messageSubstring string) bool {
for _, event := range rpt.Events {
if event.Stage == stage && event.Module == module && strings.Contains(event.Message, messageSubstring) {
return true
}
}
return false
}
func assertSegment(t *testing.T, segment model.Segment, id int, source string, sourceIndex int, speaker string, start float64, end float64, text string) {
t.Helper()

View File

@@ -13,7 +13,7 @@ const (
DefaultInputReader = "json-files"
DefaultOutputModules = "json"
DefaultPreprocessingModules = "validate-raw,normalize-speakers,trim-text"
DefaultPostprocessingModules = "detect-overlaps,resolve-overlaps,assign-ids,validate-output"
DefaultPostprocessingModules = "detect-overlaps,resolve-overlaps,autocorrect,assign-ids,validate-output"
)
// MergeOptions captures raw CLI option values before validation.
@@ -98,27 +98,13 @@ func NewMergeConfig(opts MergeOptions) (Config, error) {
cfg.AutocorrectFile = ""
}
if contains(cfg.PreprocessingModules, "normalize-speakers") {
if cfg.SpeakersFile == "" {
return Config{}, errors.New("--speakers is required when normalize-speakers is enabled")
}
if err := requireFile(cfg.SpeakersFile, "--speakers"); err != nil {
return Config{}, err
}
} else if cfg.SpeakersFile != "" {
if cfg.SpeakersFile != "" {
if err := requireFile(cfg.SpeakersFile, "--speakers"); err != nil {
return Config{}, err
}
}
if contains(cfg.PostprocessingModules, "autocorrect") {
if cfg.AutocorrectFile == "" {
return Config{}, errors.New("--autocorrect is required when autocorrect is enabled")
}
if err := requireFile(cfg.AutocorrectFile, "--autocorrect"); err != nil {
return Config{}, err
}
} else if cfg.AutocorrectFile != "" {
if cfg.AutocorrectFile != "" {
if err := requireFile(cfg.AutocorrectFile, "--autocorrect"); err != nil {
return Config{}, err
}