Implement artifact-level render command with Markdown output and update docs
This commit is contained in:
178
internal/artifact/output_artifact.go
Normal file
178
internal/artifact/output_artifact.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/seriatim/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
OutputSchemaMinimal = schema.OutputSchemaMinimal
|
||||
OutputSchemaIntermediate = schema.OutputSchemaIntermediate
|
||||
OutputSchemaFull = schema.OutputSchemaFull
|
||||
)
|
||||
|
||||
// OutputArtifact stores a parsed seriatim output artifact of one supported schema.
|
||||
type OutputArtifact struct {
|
||||
Schema string
|
||||
Full *schema.Transcript
|
||||
Intermediate *schema.IntermediateTranscript
|
||||
Minimal *schema.MinimalTranscript
|
||||
}
|
||||
|
||||
// ParseOutputArtifactJSON parses and validates serialized seriatim output JSON.
|
||||
func ParseOutputArtifactJSON(data []byte) (OutputArtifact, error) {
|
||||
var decoded any
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
return OutputArtifact{}, fmt.Errorf("input JSON is malformed: %w", err)
|
||||
}
|
||||
|
||||
var full schema.Transcript
|
||||
if err := json.Unmarshal(data, &full); err == nil {
|
||||
if err := schema.ValidateTranscript(full); err == nil {
|
||||
return OutputArtifact{
|
||||
Schema: OutputSchemaFull,
|
||||
Full: &full,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
var intermediate schema.IntermediateTranscript
|
||||
if err := json.Unmarshal(data, &intermediate); err == nil {
|
||||
if err := schema.ValidateIntermediateTranscript(intermediate); err == nil {
|
||||
return OutputArtifact{
|
||||
Schema: OutputSchemaIntermediate,
|
||||
Intermediate: &intermediate,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
var minimal schema.MinimalTranscript
|
||||
if err := json.Unmarshal(data, &minimal); err == nil {
|
||||
if err := schema.ValidateMinimalTranscript(minimal); err == nil {
|
||||
return OutputArtifact{
|
||||
Schema: OutputSchemaMinimal,
|
||||
Minimal: &minimal,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return OutputArtifact{}, fmt.Errorf("input JSON is not a valid seriatim output artifact")
|
||||
}
|
||||
|
||||
// Value returns the output payload value for serialization.
|
||||
func (artifact OutputArtifact) Value() any {
|
||||
switch artifact.Schema {
|
||||
case OutputSchemaFull:
|
||||
if artifact.Full == nil {
|
||||
return schema.Transcript{}
|
||||
}
|
||||
return *artifact.Full
|
||||
case OutputSchemaIntermediate:
|
||||
if artifact.Intermediate == nil {
|
||||
return schema.IntermediateTranscript{}
|
||||
}
|
||||
return *artifact.Intermediate
|
||||
case OutputSchemaMinimal:
|
||||
if artifact.Minimal == nil {
|
||||
return schema.MinimalTranscript{}
|
||||
}
|
||||
return *artifact.Minimal
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SegmentCount returns the number of segments in the output artifact.
|
||||
func (artifact OutputArtifact) SegmentCount() int {
|
||||
switch artifact.Schema {
|
||||
case OutputSchemaFull:
|
||||
if artifact.Full == nil {
|
||||
return 0
|
||||
}
|
||||
return len(artifact.Full.Segments)
|
||||
case OutputSchemaIntermediate:
|
||||
if artifact.Intermediate == nil {
|
||||
return 0
|
||||
}
|
||||
return len(artifact.Intermediate.Segments)
|
||||
case OutputSchemaMinimal:
|
||||
if artifact.Minimal == nil {
|
||||
return 0
|
||||
}
|
||||
return len(artifact.Minimal.Segments)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Application returns output artifact metadata application name.
|
||||
func (artifact OutputArtifact) Application() string {
|
||||
switch artifact.Schema {
|
||||
case OutputSchemaFull:
|
||||
if artifact.Full == nil {
|
||||
return ""
|
||||
}
|
||||
return artifact.Full.Metadata.Application
|
||||
case OutputSchemaIntermediate:
|
||||
if artifact.Intermediate == nil {
|
||||
return ""
|
||||
}
|
||||
return artifact.Intermediate.Metadata.Application
|
||||
case OutputSchemaMinimal:
|
||||
if artifact.Minimal == nil {
|
||||
return ""
|
||||
}
|
||||
return artifact.Minimal.Metadata.Application
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Version returns output artifact metadata version.
|
||||
func (artifact OutputArtifact) Version() string {
|
||||
switch artifact.Schema {
|
||||
case OutputSchemaFull:
|
||||
if artifact.Full == nil {
|
||||
return ""
|
||||
}
|
||||
return artifact.Full.Metadata.Version
|
||||
case OutputSchemaIntermediate:
|
||||
if artifact.Intermediate == nil {
|
||||
return ""
|
||||
}
|
||||
return artifact.Intermediate.Metadata.Version
|
||||
case OutputSchemaMinimal:
|
||||
if artifact.Minimal == nil {
|
||||
return ""
|
||||
}
|
||||
return artifact.Minimal.Metadata.Version
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// FullPayload returns the full-schema payload when present.
|
||||
func (artifact OutputArtifact) FullPayload() (*schema.Transcript, error) {
|
||||
if artifact.Full == nil {
|
||||
return nil, fmt.Errorf("full artifact payload is missing")
|
||||
}
|
||||
return artifact.Full, nil
|
||||
}
|
||||
|
||||
// IntermediatePayload returns the intermediate-schema payload when present.
|
||||
func (artifact OutputArtifact) IntermediatePayload() (*schema.IntermediateTranscript, error) {
|
||||
if artifact.Intermediate == nil {
|
||||
return nil, fmt.Errorf("intermediate artifact payload is missing")
|
||||
}
|
||||
return artifact.Intermediate, nil
|
||||
}
|
||||
|
||||
// MinimalPayload returns the minimal-schema payload when present.
|
||||
func (artifact OutputArtifact) MinimalPayload() (*schema.MinimalTranscript, error) {
|
||||
if artifact.Minimal == nil {
|
||||
return nil, fmt.Errorf("minimal artifact payload is missing")
|
||||
}
|
||||
return artifact.Minimal, nil
|
||||
}
|
||||
134
internal/artifact/output_artifact_test.go
Normal file
134
internal/artifact/output_artifact_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/seriatim/schema"
|
||||
)
|
||||
|
||||
func TestParseOutputArtifactJSONParsesFullIntermediateAndMinimal(t *testing.T) {
|
||||
t.Run("full", func(t *testing.T) {
|
||||
first := 0
|
||||
value := schema.Transcript{
|
||||
Metadata: schema.Metadata{
|
||||
Application: "seriatim",
|
||||
Version: "v-test",
|
||||
InputReader: "json-files",
|
||||
InputFiles: []string{"input.json"},
|
||||
PreprocessingModules: []string{"validate-raw"},
|
||||
PostprocessingModules: []string{"assign-ids", "validate-output"},
|
||||
OutputModules: []string{"json"},
|
||||
},
|
||||
Segments: []schema.Segment{
|
||||
{
|
||||
ID: 1,
|
||||
Source: "input.json",
|
||||
SourceSegmentIndex: &first,
|
||||
Speaker: "Alice",
|
||||
Start: 1,
|
||||
End: 2,
|
||||
Text: "hello",
|
||||
Categories: []string{"backchannel"},
|
||||
},
|
||||
},
|
||||
OverlapGroups: []schema.OverlapGroup{},
|
||||
}
|
||||
|
||||
parsed := mustParseOutputArtifact(t, value)
|
||||
if parsed.Schema != OutputSchemaFull {
|
||||
t.Fatalf("schema = %q, want %q", parsed.Schema, OutputSchemaFull)
|
||||
}
|
||||
if parsed.Full == nil {
|
||||
t.Fatal("expected full payload")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("intermediate", func(t *testing.T) {
|
||||
value := schema.IntermediateTranscript{
|
||||
Metadata: schema.IntermediateMetadata{
|
||||
Application: "seriatim",
|
||||
Version: "v-test",
|
||||
OutputSchema: OutputSchemaIntermediate,
|
||||
},
|
||||
Segments: []schema.IntermediateSegment{
|
||||
{ID: 1, Start: 1, End: 2, Speaker: "Alice", Text: "hello", Categories: []string{"filler"}},
|
||||
},
|
||||
}
|
||||
|
||||
parsed := mustParseOutputArtifact(t, value)
|
||||
if parsed.Schema != OutputSchemaIntermediate {
|
||||
t.Fatalf("schema = %q, want %q", parsed.Schema, OutputSchemaIntermediate)
|
||||
}
|
||||
if parsed.Intermediate == nil {
|
||||
t.Fatal("expected intermediate payload")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("minimal", func(t *testing.T) {
|
||||
value := schema.MinimalTranscript{
|
||||
Metadata: schema.MinimalMetadata{
|
||||
Application: "seriatim",
|
||||
Version: "v-test",
|
||||
OutputSchema: OutputSchemaMinimal,
|
||||
},
|
||||
Segments: []schema.MinimalSegment{
|
||||
{ID: 1, Start: 1, End: 2, Speaker: "Alice", Text: "hello"},
|
||||
},
|
||||
}
|
||||
|
||||
parsed := mustParseOutputArtifact(t, value)
|
||||
if parsed.Schema != OutputSchemaMinimal {
|
||||
t.Fatalf("schema = %q, want %q", parsed.Schema, OutputSchemaMinimal)
|
||||
}
|
||||
if parsed.Minimal == nil {
|
||||
t.Fatal("expected minimal payload")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseOutputArtifactJSONRejectsMalformedJSON(t *testing.T) {
|
||||
_, err := ParseOutputArtifactJSON([]byte(`{"metadata":`))
|
||||
if err == nil {
|
||||
t.Fatal("expected malformed JSON error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "input JSON is malformed") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOutputArtifactJSONRejectsRawWhisperXLikeInput(t *testing.T) {
|
||||
data := []byte(`{
|
||||
"segments": [
|
||||
{
|
||||
"id": 0,
|
||||
"start": 0.1,
|
||||
"end": 1.2,
|
||||
"text": "hello",
|
||||
"words": [{"word":"hello","start":0.1,"end":0.8}]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
_, err := ParseOutputArtifactJSON(data)
|
||||
if err == nil {
|
||||
t.Fatal("expected artifact validation error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not a valid seriatim output artifact") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustParseOutputArtifact(t *testing.T, value any) OutputArtifact {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
parsed, err := ParseOutputArtifactJSON(data)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
Reference in New Issue
Block a user