Organize generic and Seriatim modules by domain
This commit is contained in:
179
internal/modules/seriatim/input/transcript/model.go
Normal file
179
internal/modules/seriatim/input/transcript/model.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package transcript
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type transcript struct {
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Segments []segment `json:"segments"`
|
||||
}
|
||||
|
||||
type segment struct {
|
||||
ID int `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 fields map[string]json.RawMessage
|
||||
if err := decodeJSON(raw, &fields); err != nil {
|
||||
return transcript{}, err
|
||||
}
|
||||
if fields == nil {
|
||||
return transcript{}, fmt.Errorf("top-level value must be an object")
|
||||
}
|
||||
|
||||
metadataRaw, ok := fields["metadata"]
|
||||
if !ok {
|
||||
return transcript{}, fmt.Errorf("metadata is required")
|
||||
}
|
||||
var metadata map[string]any
|
||||
if err := decodeJSON(metadataRaw, &metadata); err != nil {
|
||||
return transcript{}, fmt.Errorf("metadata must be an object: %w", err)
|
||||
}
|
||||
if metadata == nil {
|
||||
return transcript{}, fmt.Errorf("metadata must be an object")
|
||||
}
|
||||
|
||||
segmentsRaw, ok := fields["segments"]
|
||||
if !ok {
|
||||
return transcript{}, fmt.Errorf("segments are required")
|
||||
}
|
||||
var segmentValues []json.RawMessage
|
||||
if err := decodeJSON(segmentsRaw, &segmentValues); err != nil {
|
||||
return transcript{}, fmt.Errorf("segments must be an array: %w", err)
|
||||
}
|
||||
if segmentValues == nil {
|
||||
return transcript{}, fmt.Errorf("segments must be an array")
|
||||
}
|
||||
segments := make([]segment, 0, len(segmentValues))
|
||||
for i, rawSegment := range segmentValues {
|
||||
segment, err := decodeSegment(rawSegment, i)
|
||||
if err != nil {
|
||||
return transcript{}, err
|
||||
}
|
||||
segments = append(segments, segment)
|
||||
}
|
||||
|
||||
return transcript{
|
||||
Metadata: metadata,
|
||||
Segments: segments,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeSegment(raw []byte, index int) (segment, error) {
|
||||
var fields map[string]json.RawMessage
|
||||
if err := decodeJSON(raw, &fields); err != nil {
|
||||
return segment{}, fmt.Errorf("segment[%d] must be an object: %w", index, err)
|
||||
}
|
||||
if fields == nil {
|
||||
return segment{}, fmt.Errorf("segment[%d] must be an object", index)
|
||||
}
|
||||
|
||||
var decoded segment
|
||||
if err := decodeOptionalSegmentID(fields, "id", &decoded.ID); err != nil {
|
||||
return segment{}, fmt.Errorf("segment[%d] id must be a positive integer 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)
|
||||
}
|
||||
if err := decodeOptionalNumber(fields, "end", &decoded.End); err != nil {
|
||||
return segment{}, fmt.Errorf("segment[%d] end must be a number: %w", index, err)
|
||||
}
|
||||
if err := decodeOptionalString(fields, "speaker", &decoded.Speaker); err != nil {
|
||||
return segment{}, fmt.Errorf("segment[%d] speaker must be a string: %w", index, err)
|
||||
}
|
||||
if err := decodeOptionalString(fields, "text", &decoded.Text); err != nil {
|
||||
return segment{}, fmt.Errorf("segment[%d] text must be a string: %w", index, err)
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
func decodeOptionalString(fields map[string]json.RawMessage, key string, out *string) error {
|
||||
raw, ok := fields[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return decodeJSON(raw, out)
|
||||
}
|
||||
|
||||
func decodeOptionalSegmentID(fields map[string]json.RawMessage, key string, out *int) error {
|
||||
raw, ok := fields[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var text string
|
||||
if err := decodeJSON(raw, &text); err == nil {
|
||||
parsed, err := parsePositiveInt(text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*out = parsed
|
||||
return nil
|
||||
}
|
||||
|
||||
var number json.Number
|
||||
if err := decodeJSON(raw, &number); err == nil {
|
||||
parsed, err := parsePositiveInt(number.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*out = parsed
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("must be a positive integer string or number")
|
||||
}
|
||||
|
||||
func parsePositiveInt(value string) (int, error) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return 0, fmt.Errorf("must not be empty")
|
||||
}
|
||||
if trimmed != value {
|
||||
return 0, fmt.Errorf("must not contain leading or trailing whitespace")
|
||||
}
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("must be an integer")
|
||||
}
|
||||
if parsed <= 0 {
|
||||
return 0, fmt.Errorf("must be positive")
|
||||
}
|
||||
if strconv.Itoa(parsed) != value {
|
||||
return 0, fmt.Errorf("must be a canonical positive integer")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func decodeOptionalNumber(fields map[string]json.RawMessage, key string, out *json.Number) error {
|
||||
raw, ok := fields[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return decodeJSON(raw, out)
|
||||
}
|
||||
|
||||
func decodeJSON(raw []byte, out any) error {
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
decoder.UseNumber()
|
||||
if err := decoder.Decode(out); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
||||
if err == nil {
|
||||
return fmt.Errorf("unexpected trailing JSON value")
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user