From b85e826c1c5535729fb6b2fb16caf728edcea242 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 3 Jul 2026 22:27:54 -0500 Subject: [PATCH] Bugfix in the seriatim input adapter --- README.md | 6 ++-- docs/config.md | 2 +- docs/integrations/seriatim.md | 23 ++++++++----- docs/internal/modules.md | 6 ++-- internal/modules/input/seriatim/adapter.go | 2 +- .../modules/input/seriatim/adapter_test.go | 34 +++++++++++++++++++ internal/modules/input/seriatim/model.go | 25 ++++++++++++-- 7 files changed, 80 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index cbf81be..1f81c21 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ Notarius is a Go CLI for extracting structured artifacts from source material with explicit, configurable pipeline modules. -The current implementation reads Seriatim minimal transcript JSON, chunks the -source units, extracts D&D spell-cast artifacts with an OpenAI-compatible LLM, -and writes JSON output plus diagnostics for each run. +The current implementation reads Seriatim transcript JSON, chunks the source +units, extracts D&D spell-cast artifacts with an OpenAI-compatible LLM, and +writes JSON output plus diagnostics for each run. ```sh NOTARIUS_LLM_DEFAULT_BASE_URL=http://127.0.0.1:8080/v1 \ diff --git a/docs/config.md b/docs/config.md index 7670b6d..4ba29c9 100644 --- a/docs/config.md +++ b/docs/config.md @@ -165,7 +165,7 @@ one configured profile. | Slot | Key | Notes | | --- | --- | --- | -| input | `seriatim` | Reads Seriatim minimal transcript JSON. | +| input | `seriatim` | Reads Seriatim transcript JSON. | | chunk | `generic` | Splits source units into ordered chunks. | | extract | `dnd/spells` | Extracts `dnd.spell_cast` artifacts. | | merge | `appendorder` | Keeps candidates in append order. | diff --git a/docs/integrations/seriatim.md b/docs/integrations/seriatim.md index 0587362..f706f3c 100644 --- a/docs/integrations/seriatim.md +++ b/docs/integrations/seriatim.md @@ -1,4 +1,4 @@ -# Seriatim Minimal Transcript JSON +# Seriatim Transcript JSON This document is the external input contract for the implemented `seriatim` input adapter. @@ -8,7 +8,7 @@ input adapter. - Module key: `seriatim` - Document kind: `transcript` - Unit kind: `transcript_segment` -- Source format: `application/vnd.seriatim.minimal+json` +- Source format: `application/vnd.seriatim+json` The adapter parses raw Seriatim JSON into a generic source document. It owns transcript-specific JSON parsing and metadata mapping; core source and pipeline @@ -17,7 +17,8 @@ code stay source-format agnostic. ## Accepted Shape The input must be one JSON object with top-level `metadata` and `segments` -fields: +fields. This covers the maintained minimal fixture and Seriatim intermediate +output that provides the same required segment fields. ```json { @@ -40,7 +41,8 @@ fields: The maintained example is [examples/seriatim-minimal-transcript.json](../../examples/seriatim-minimal-transcript.json). -Top-level metadata entries are preserved. Other segment fields are ignored. +Top-level metadata entries are preserved. Other segment fields, such as +`categories`, are ignored. Multiple top-level JSON values are rejected. @@ -54,7 +56,8 @@ The adapter rejects: - missing, null, or non-object `metadata`; - missing, null, non-array, or empty `segments`; - segment values that are not objects; -- non-string `id`, `speaker`, or `text`; +- segment `id` values that are neither strings nor numbers; +- non-string `speaker` or `text`; - empty segment IDs; - segment IDs with leading or trailing whitespace; - duplicate segment IDs; @@ -72,7 +75,7 @@ The adapter maps input to `SourceDocument`: - `metadata` becomes `SourceDocument.Metadata`; - `SourceDocument.Kind` is `transcript`; -- `SourceDocument.Format` is `application/vnd.seriatim.minimal+json`; +- `SourceDocument.Format` is `application/vnd.seriatim+json`; - `SourceDocument.Digest` is `sha256:` of the exact raw input bytes. `SourceDocument.ID` is selected in this order: @@ -84,7 +87,8 @@ The adapter maps input to `SourceDocument`: Each segment becomes one `SourceUnit`: -- `segment.id` becomes `SourceUnit.ID`; +- `segment.id` becomes `SourceUnit.ID`; numeric IDs are converted to their JSON + number text, so `1` becomes `"1"`; - `segment.text` becomes `SourceUnit.Text`; - `SourceUnit.Kind` is `transcript_segment`; - `speaker`, `start`, and `end` are stored in source-unit metadata. @@ -110,4 +114,7 @@ The module declares these provided capabilities: ## Compatibility Limit -This contract covers only the minimal transcript JSON shape described here. +This contract covers only Seriatim transcript JSON with the top-level +`metadata` object and `segments` array described here. Broader Seriatim output +schemas are compatible only when they provide these required fields with the +accepted types. diff --git a/docs/internal/modules.md b/docs/internal/modules.md index be818c2..1f62371 100644 --- a/docs/internal/modules.md +++ b/docs/internal/modules.md @@ -24,9 +24,9 @@ reject incompatible pipelines before execution. Package: `internal/modules/input/seriatim` -The `seriatim` adapter parses Seriatim minimal transcript JSON into a generic -source document. It owns transcript JSON details, source ID selection, source -digest creation, transcript segment validation, and segment metadata mapping. +The `seriatim` adapter parses Seriatim transcript JSON into a generic source +document. It owns transcript JSON details, source ID selection, source digest +creation, transcript segment validation, and segment metadata mapping. Provides: diff --git a/internal/modules/input/seriatim/adapter.go b/internal/modules/input/seriatim/adapter.go index 5da7c58..2652d50 100644 --- a/internal/modules/input/seriatim/adapter.go +++ b/internal/modules/input/seriatim/adapter.go @@ -20,7 +20,7 @@ const Key = "seriatim" const ( DocumentKind = "transcript" UnitKind = "transcript_segment" - Format = "application/vnd.seriatim.minimal+json" + Format = "application/vnd.seriatim+json" ) var providedCapabilities = []string{ diff --git a/internal/modules/input/seriatim/adapter_test.go b/internal/modules/input/seriatim/adapter_test.go index baa630a..3178f51 100644 --- a/internal/modules/input/seriatim/adapter_test.go +++ b/internal/modules/input/seriatim/adapter_test.go @@ -70,6 +70,35 @@ func TestParseValidMinimalTranscript(t *testing.T) { } } +func TestParseAcceptsNumericSegmentIDs(t *testing.T) { + raw := []byte(`{"metadata":{"application":"seriatim","version":"v1.5.0","output_schema":"seriatim-intermediate"},"segments":[{"id":1,"start":451.821,"end":469.685,"speaker":"Narrator","text":"The stone door opens.","categories":["scene"]},{"id":2,"start":470,"end":471,"speaker":"Player","text":"I step inside."}]}`) + + doc, err := New().Parse(context.Background(), contracts.ParseRequest{Raw: raw}) + if err != nil { + t.Fatalf("Parse() error = %v, want nil", err) + } + if got, want := doc.Metadata["output_schema"], "seriatim-intermediate"; got != want { + t.Fatalf("doc.Metadata[output_schema] = %#v, want %q", got, want) + } + if doc.Format != Format { + t.Fatalf("doc.Format = %q, want %q", doc.Format, Format) + } + if len(doc.Units) != 2 { + t.Fatalf("len(doc.Units) = %d, want 2", len(doc.Units)) + } + if doc.Units[0].ID != "1" || doc.Units[1].ID != "2" { + t.Fatalf("unit IDs = %#v, want numeric IDs normalized to strings", []string{doc.Units[0].ID, doc.Units[1].ID}) + } + ref := source.SourceRef{ + SourceID: doc.ID, + StartUnitID: "1", + EndUnitID: "2", + } + if err := source.ValidateRef(doc, ref); err != nil { + t.Fatalf("ValidateRef() error = %v, want nil", err) + } +} + func TestParseRequestSourceIDOverridesMetadataIDs(t *testing.T) { raw := readFixture(t, "testdata/valid_minimal.json") @@ -176,6 +205,11 @@ func TestParseRejectsInvalidInput(t *testing.T) { raw: validJSONWithSegment(`"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."`), wantErr: []string{"id", "empty"}, }, + { + name: "invalid segment id type", + raw: validJSONWithSegment(`"id":{},"start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."`), + wantErr: []string{"id", "string or number"}, + }, { name: "whitespace segment id", raw: validJSONWithSegment(`"id":" s1 ","start":0,"end":1,"speaker":"Narrator","text":"Synthetic text."`), diff --git a/internal/modules/input/seriatim/model.go b/internal/modules/input/seriatim/model.go index 2cdd123..56bfb84 100644 --- a/internal/modules/input/seriatim/model.go +++ b/internal/modules/input/seriatim/model.go @@ -77,8 +77,8 @@ func decodeSegment(raw []byte, index int) (segment, error) { } var decoded segment - if err := decodeOptionalString(fields, "id", &decoded.ID); err != nil { - return segment{}, fmt.Errorf("segment[%d] id must be a string: %w", index, err) + if err := decodeOptionalSegmentID(fields, "id", &decoded.ID); err != nil { + return segment{}, fmt.Errorf("segment[%d] id must be a string or number: %w", index, err) } if err := decodeOptionalNumber(fields, "start", &decoded.Start); err != nil { return segment{}, fmt.Errorf("segment[%d] start must be a number: %w", index, err) @@ -103,6 +103,27 @@ func decodeOptionalString(fields map[string]json.RawMessage, key string, out *st return decodeJSON(raw, out) } +func decodeOptionalSegmentID(fields map[string]json.RawMessage, key string, out *string) error { + raw, ok := fields[key] + if !ok { + return nil + } + + var text string + if err := decodeJSON(raw, &text); err == nil { + *out = text + return nil + } + + var number json.Number + if err := decodeJSON(raw, &number); err == nil { + *out = number.String() + return nil + } + + return fmt.Errorf("must be a string or number") +} + func decodeOptionalNumber(fields map[string]json.RawMessage, key string, out *json.Number) error { raw, ok := fields[key] if !ok {