From 47013daa04aacef276be500301d7edeea3256c24 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 3 Jul 2026 21:09:43 +0000 Subject: [PATCH] Add Seriatim input module skeleton --- internal/modules/input/seriatim/adapter.go | 54 +++++++++ internal/modules/input/seriatim/metadata.go | 28 +++++ internal/modules/input/seriatim/model.go | 29 +++++ .../modules/input/seriatim/registry_test.go | 106 ++++++++++++++++++ 4 files changed, 217 insertions(+) create mode 100644 internal/modules/input/seriatim/adapter.go create mode 100644 internal/modules/input/seriatim/metadata.go create mode 100644 internal/modules/input/seriatim/model.go create mode 100644 internal/modules/input/seriatim/registry_test.go diff --git a/internal/modules/input/seriatim/adapter.go b/internal/modules/input/seriatim/adapter.go new file mode 100644 index 0000000..f6f9aa6 --- /dev/null +++ b/internal/modules/input/seriatim/adapter.go @@ -0,0 +1,54 @@ +package seriatim + +import ( + "context" + "fmt" + + "gitea.maximumdirect.net/eric/notarius/internal/core/source" + "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" + "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" +) + +const Key = "seriatim" + +const ( + DocumentKind = "transcript" + UnitKind = "transcript_segment" + Format = "application/vnd.seriatim.minimal+json" +) + +var providedCapabilities = []string{ + "source.transcript", + "transcript.speaker", + "transcript.timestamps", +} + +var _ contracts.InputAdapter = (*Adapter)(nil) + +type Adapter struct{} + +func New() *Adapter { + return &Adapter{} +} + +func (a *Adapter) Key() string { + return Key +} + +func (a *Adapter) Parse(ctx context.Context, req contracts.ParseRequest) (*source.SourceDocument, error) { + return nil, fmt.Errorf("seriatim input: source document parsing is not implemented") +} + +func ModuleSpec() pipeline.ModuleSpec { + return pipeline.ModuleSpec{ + Key: Key, + Stage: pipeline.StageInput, + Provides: append([]string(nil), providedCapabilities...), + } +} + +func Register(registry *pipeline.InputAdapterRegistry) error { + return registry.RegisterWithSpec(ModuleSpec(), func() (contracts.InputAdapter, error) { + return New(), nil + }) +} diff --git a/internal/modules/input/seriatim/metadata.go b/internal/modules/input/seriatim/metadata.go new file mode 100644 index 0000000..b439261 --- /dev/null +++ b/internal/modules/input/seriatim/metadata.go @@ -0,0 +1,28 @@ +package seriatim + +import ( + "encoding/json" + + "gitea.maximumdirect.net/eric/notarius/internal/core/source" +) + +const ( + MetadataSpeaker = "speaker" + MetadataStart = "start" + MetadataEnd = "end" +) + +func Speaker(unit source.SourceUnit) (string, bool) { + value, ok := unit.Metadata[MetadataSpeaker].(string) + return value, ok +} + +func Start(unit source.SourceUnit) (json.Number, bool) { + value, ok := unit.Metadata[MetadataStart].(json.Number) + return value, ok +} + +func End(unit source.SourceUnit) (json.Number, bool) { + value, ok := unit.Metadata[MetadataEnd].(json.Number) + return value, ok +} diff --git a/internal/modules/input/seriatim/model.go b/internal/modules/input/seriatim/model.go new file mode 100644 index 0000000..66202bb --- /dev/null +++ b/internal/modules/input/seriatim/model.go @@ -0,0 +1,29 @@ +package seriatim + +import ( + "bytes" + "encoding/json" +) + +type transcript struct { + Metadata map[string]any `json:"metadata"` + Segments []segment `json:"segments"` +} + +type segment struct { + ID string `json:"id"` + Start json.Number `json:"start"` + End json.Number `json:"end"` + Speaker string `json:"speaker"` + Text string `json:"text"` +} + +func decodeTranscript(raw []byte) (transcript, error) { + var out transcript + decoder := json.NewDecoder(bytes.NewReader(raw)) + decoder.UseNumber() + if err := decoder.Decode(&out); err != nil { + return transcript{}, err + } + return out, nil +} diff --git a/internal/modules/input/seriatim/registry_test.go b/internal/modules/input/seriatim/registry_test.go new file mode 100644 index 0000000..b25c8b4 --- /dev/null +++ b/internal/modules/input/seriatim/registry_test.go @@ -0,0 +1,106 @@ +package seriatim + +import ( + "encoding/json" + "reflect" + "strings" + "testing" + + "gitea.maximumdirect.net/eric/notarius/internal/core/source" + "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" +) + +func TestNewReturnsAdapterWithKey(t *testing.T) { + adapter := New() + if adapter == nil { + t.Fatal("New() = nil, want adapter") + } + if adapter.Key() != Key { + t.Fatalf("adapter.Key() = %q, want %q", adapter.Key(), Key) + } +} + +func TestModuleSpec(t *testing.T) { + got := ModuleSpec() + want := pipeline.ModuleSpec{ + Key: Key, + Stage: pipeline.StageInput, + Provides: []string{ + "source.transcript", + "transcript.speaker", + "transcript.timestamps", + }, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("ModuleSpec() = %#v, want %#v", got, want) + } + + got.Provides[0] = "changed" + again := ModuleSpec() + if !reflect.DeepEqual(again, want) { + t.Fatalf("ModuleSpec() after caller mutation = %#v, want %#v", again, want) + } +} + +func TestRegisterMakesAdapterBuildable(t *testing.T) { + registry := pipeline.NewInputAdapterRegistry() + + if err := Register(registry); err != nil { + t.Fatalf("Register() error = %v, want nil", err) + } + + adapter, err := registry.Build(Key) + if err != nil { + t.Fatalf("Build() error = %v, want nil", err) + } + if adapter.Key() != Key { + t.Fatalf("adapter.Key() = %q, want %q", adapter.Key(), Key) + } +} + +func TestRegisterStoresModuleSpec(t *testing.T) { + registry := pipeline.NewInputAdapterRegistry() + + if err := Register(registry); err != nil { + t.Fatalf("Register() error = %v, want nil", err) + } + + got, ok := registry.Spec(Key) + if !ok { + t.Fatal("Spec() ok = false, want true") + } + want := ModuleSpec() + if !reflect.DeepEqual(got, want) { + t.Fatalf("Spec() = %#v, want %#v", got, want) + } +} + +func TestRegisterNilRegistryReturnsError(t *testing.T) { + err := Register(nil) + if err == nil { + t.Fatal("Register(nil) error = nil, want error") + } + if !strings.Contains(err.Error(), "input adapter registry") { + t.Fatalf("Register(nil) error = %q, want registry context", err.Error()) + } +} + +func TestMetadataHelpers(t *testing.T) { + unit := source.SourceUnit{ + Metadata: map[string]any{ + MetadataSpeaker: "Narrator", + MetadataStart: json.Number("1.25"), + MetadataEnd: json.Number("2.5"), + }, + } + + if got, ok := Speaker(unit); !ok || got != "Narrator" { + t.Fatalf("Speaker() = %q, %v; want Narrator, true", got, ok) + } + if got, ok := Start(unit); !ok || got != json.Number("1.25") { + t.Fatalf("Start() = %q, %v; want 1.25, true", got, ok) + } + if got, ok := End(unit); !ok || got != json.Number("2.5") { + t.Fatalf("End() = %q, %v; want 2.5, true", got, ok) + } +}