2026-04-26 13:03:07 -05:00
package builtin
import (
"context"
"fmt"
2026-04-26 19:47:47 -05:00
"path/filepath"
2026-04-26 13:57:13 -05:00
"strings"
2026-04-26 13:03:07 -05:00
"gitea.maximumdirect.net/eric/seriatim/internal/config"
"gitea.maximumdirect.net/eric/seriatim/internal/model"
"gitea.maximumdirect.net/eric/seriatim/internal/pipeline"
"gitea.maximumdirect.net/eric/seriatim/internal/report"
2026-04-26 13:57:13 -05:00
"gitea.maximumdirect.net/eric/seriatim/internal/speaker"
2026-04-26 13:03:07 -05:00
)
2026-04-27 21:48:04 -05:00
type validateRaw struct { }
2026-04-26 13:03:07 -05:00
2026-04-27 21:48:04 -05:00
func ( validateRaw ) Name ( ) string {
return "validate-raw"
2026-04-26 13:03:07 -05:00
}
2026-04-27 21:48:04 -05:00
func ( validateRaw ) Requires ( ) pipeline . ModelState {
return pipeline . StateRaw
2026-04-26 13:03:07 -05:00
}
2026-04-27 21:48:04 -05:00
func ( validateRaw ) Produces ( ) pipeline . ModelState {
return pipeline . StateRaw
2026-04-26 13:03:07 -05:00
}
2026-04-27 21:48:04 -05:00
func ( validateRaw ) Process ( ctx context . Context , in pipeline . PreprocessState , cfg config . Config ) ( pipeline . PreprocessState , [ ] report . Event , error ) {
2026-04-26 13:03:07 -05:00
if err := ctx . Err ( ) ; err != nil {
return pipeline . PreprocessState { } , nil , err
}
2026-04-27 21:48:04 -05:00
if in . State != pipeline . StateRaw {
return pipeline . PreprocessState { } , nil , fmt . Errorf ( "preprocessing module %q requires state %q but received %q" , "validate-raw" , pipeline . StateRaw , in . State )
}
if len ( in . Raw ) == 0 {
return pipeline . PreprocessState { } , nil , fmt . Errorf ( "validate-raw: no raw transcript(s) to validate" )
}
for transcriptIndex , transcript := range in . Raw {
if strings . TrimSpace ( transcript . Source ) == "" {
return pipeline . PreprocessState { } , nil , fmt . Errorf ( "validate-raw: raw transcript %d has empty source" , transcriptIndex )
}
for segmentIndex , segment := range transcript . Segments {
if segment . Start < 0 {
return pipeline . PreprocessState { } , nil , fmt . Errorf ( "validate-raw: raw transcript %q segment %d has negative start" , transcript . Source , segmentIndex )
}
if segment . End < segment . Start {
return pipeline . PreprocessState { } , nil , fmt . Errorf ( "validate-raw: raw transcript %q segment %d has end before start" , transcript . Source , segmentIndex )
}
for wordIndex , word := range segment . Words {
if ! word . Timed {
continue
}
if word . Start < 0 {
return pipeline . PreprocessState { } , nil , fmt . Errorf ( "validate-raw: raw transcript %q segment %d word %d has negative start" , transcript . Source , segmentIndex , wordIndex )
}
if word . End < word . Start {
return pipeline . PreprocessState { } , nil , fmt . Errorf ( "validate-raw: raw transcript %q segment %d word %d has end before start" , transcript . Source , segmentIndex , wordIndex )
}
}
}
2026-04-26 13:03:07 -05:00
}
return in , [ ] report . Event {
2026-04-27 21:48:04 -05:00
report . Info ( "preprocessing" , "validate-raw" , fmt . Sprintf ( "validated %d raw transcript(s)" , len ( in . Raw ) ) ) ,
2026-04-26 13:03:07 -05:00
} , nil
}
2026-04-26 13:57:13 -05:00
type trimText struct { }
func ( trimText ) Name ( ) string {
return "trim-text"
}
func ( trimText ) Requires ( ) pipeline . ModelState {
return pipeline . StateCanonical
}
func ( trimText ) Produces ( ) pipeline . ModelState {
return pipeline . StateCanonical
}
func ( trimText ) Process ( ctx context . Context , in pipeline . PreprocessState , cfg config . Config ) ( pipeline . PreprocessState , [ ] report . Event , error ) {
if err := ctx . Err ( ) ; err != nil {
return pipeline . PreprocessState { } , nil , err
}
if in . State != pipeline . StateCanonical {
return pipeline . PreprocessState { } , nil , fmt . Errorf ( "preprocessing module %q requires state %q but received %q" , "trim-text" , pipeline . StateCanonical , in . State )
}
for transcriptIndex := range in . Canonical {
for segmentIndex := range in . Canonical [ transcriptIndex ] . Segments {
in . Canonical [ transcriptIndex ] . Segments [ segmentIndex ] . Text = strings . TrimSpace ( in . Canonical [ transcriptIndex ] . Segments [ segmentIndex ] . Text )
}
}
return in , [ ] report . Event {
report . Info ( "preprocessing" , "trim-text" , "trimmed canonical segment text" ) ,
} , nil
}
2026-04-26 13:03:07 -05:00
type normalizeSpeakers struct { }
func ( normalizeSpeakers ) Name ( ) string {
return "normalize-speakers"
}
func ( normalizeSpeakers ) Requires ( ) pipeline . ModelState {
return pipeline . StateRaw
}
func ( normalizeSpeakers ) Produces ( ) pipeline . ModelState {
return pipeline . StateCanonical
}
func ( normalizeSpeakers ) Process ( ctx context . Context , in pipeline . PreprocessState , cfg config . Config ) ( pipeline . PreprocessState , [ ] report . Event , error ) {
if err := ctx . Err ( ) ; err != nil {
return pipeline . PreprocessState { } , nil , err
}
if in . State != pipeline . StateRaw {
return pipeline . PreprocessState { } , nil , fmt . Errorf ( "preprocessing module %q requires state %q but received %q" , "normalize-speakers" , pipeline . StateRaw , in . State )
}
2026-04-26 19:47:47 -05:00
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
}
2026-04-26 13:57:13 -05:00
}
2026-04-26 13:03:07 -05:00
canonical := make ( [ ] model . CanonicalTranscript , 0 , len ( in . Raw ) )
for _ , raw := range in . Raw {
2026-04-26 19:47:47 -05:00
canonicalSpeaker := filepath . Base ( raw . Source )
if useSpeakerMap {
var err error
canonicalSpeaker , err = speakers . SpeakerForSource ( raw . Source )
if err != nil {
return pipeline . PreprocessState { } , nil , err
}
2026-04-26 13:57:13 -05:00
}
segments := make ( [ ] model . Segment , 0 , len ( raw . Segments ) )
for index , rawSegment := range raw . Segments {
2026-04-27 15:52:53 -05:00
sourceSegmentIndex := index
2026-04-26 13:57:13 -05:00
segments = append ( segments , model . Segment {
Source : raw . Source ,
2026-04-27 15:52:53 -05:00
SourceSegmentIndex : & sourceSegmentIndex ,
2026-04-26 13:57:13 -05:00
Speaker : canonicalSpeaker ,
Start : rawSegment . Start ,
End : rawSegment . End ,
Text : rawSegment . Text ,
2026-04-27 15:52:53 -05:00
Words : append ( [ ] model . Word ( nil ) , rawSegment . Words ... ) ,
2026-04-26 13:57:13 -05:00
} )
}
2026-04-26 13:03:07 -05:00
canonical = append ( canonical , model . CanonicalTranscript {
Source : raw . Source ,
2026-04-26 13:57:13 -05:00
Segments : segments ,
2026-04-26 13:03:07 -05:00
} )
}
2026-04-26 19:47:47 -05:00
message := "created canonical transcript(s) from raw input"
if ! useSpeakerMap {
message = "created canonical transcript(s) using input basenames as speaker labels"
}
2026-04-26 13:03:07 -05:00
return pipeline . PreprocessState {
State : pipeline . StateCanonical ,
Raw : append ( [ ] model . RawTranscript ( nil ) , in . Raw ... ) ,
Canonical : canonical ,
} , [ ] report . Event {
2026-04-26 19:47:47 -05:00
report . Info ( "preprocessing" , "normalize-speakers" , message ) ,
2026-04-26 13:03:07 -05:00
} , nil
}