Add durable speaker transcription state
This commit is contained in:
128
internal/manifest/transcribe_state.go
Normal file
128
internal/manifest/transcribe_state.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// TranscribeStateContractVersion identifies the supported per-speaker
|
||||
// completion representation owned by the transcribe stage.
|
||||
TranscribeStateContractVersion = 1
|
||||
// TranscribeFingerprintContractVersion identifies the fingerprint
|
||||
// representation stored by the supported transcribe state contract.
|
||||
TranscribeFingerprintContractVersion = 1
|
||||
)
|
||||
|
||||
// TranscribeSpeakerRecord binds one completed raw transcript to the audio and
|
||||
// observable recognition settings that produced it.
|
||||
type TranscribeSpeakerRecord struct {
|
||||
FingerprintVersion int `json:"fingerprint_version"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
AudioPath string `json:"audio_path"`
|
||||
AudioSHA256 string `json:"audio_sha256"`
|
||||
AudioSize int64 `json:"audio_size"`
|
||||
Output ArtifactRecord `json:"output"`
|
||||
OutputSize int64 `json:"output_size"`
|
||||
ProducerRunID string `json:"producer_run_id"`
|
||||
}
|
||||
|
||||
// HasVersionedTranscribeState reports whether a transcribe stage record carries
|
||||
// the supported per-speaker authority. An aggregate-only record returns false.
|
||||
func (s *StageRecord) HasVersionedTranscribeState() bool {
|
||||
return s != nil && s.Name == "transcribe" && s.TranscribeStateVersion == TranscribeStateContractVersion
|
||||
}
|
||||
|
||||
// ValidateTranscribeSpeakerCollection validates one complete session
|
||||
// collection independently of its containing manifest.
|
||||
func ValidateTranscribeSpeakerCollection(version int, records map[string]TranscribeSpeakerRecord) error {
|
||||
if version != TranscribeStateContractVersion {
|
||||
return fmt.Errorf("unsupported transcribe state version %d", version)
|
||||
}
|
||||
for speakerID, record := range records {
|
||||
if err := validateTranscribeSpeakerRecord(speakerID, record); err != nil {
|
||||
return fmt.Errorf("transcribe speaker %q: %w", speakerID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloneTranscribeSpeakerCollection returns a deep copy so checkpoint candidates
|
||||
// and installed authority cannot share mutable artifact metadata.
|
||||
func CloneTranscribeSpeakerCollection(records map[string]TranscribeSpeakerRecord) map[string]TranscribeSpeakerRecord {
|
||||
if records == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[string]TranscribeSpeakerRecord, len(records))
|
||||
for speakerID, record := range records {
|
||||
output := cloneArtifactRecord(&record.Output)
|
||||
record.Output = *output
|
||||
cloned[speakerID] = record
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func validateTranscribeStageState(stageName string, version int, records map[string]TranscribeSpeakerRecord) error {
|
||||
if stageName != "transcribe" {
|
||||
if version != 0 || records != nil {
|
||||
return fmt.Errorf("stage %q cannot contain transcribe-owned state", stageName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if version == 0 && records == nil {
|
||||
return nil
|
||||
}
|
||||
return ValidateTranscribeSpeakerCollection(version, records)
|
||||
}
|
||||
|
||||
func validateTranscribeSpeakerRecord(speakerID string, record TranscribeSpeakerRecord) error {
|
||||
if speakerID == "" {
|
||||
return fmt.Errorf("speaker ID is required")
|
||||
}
|
||||
if record.FingerprintVersion != TranscribeFingerprintContractVersion {
|
||||
return fmt.Errorf("unsupported fingerprint version %d", record.FingerprintVersion)
|
||||
}
|
||||
if err := validateSHA256("fingerprint", record.Fingerprint); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateCanonicalRelativePath("audio_path", record.AudioPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateSHA256("audio_sha256", record.AudioSHA256); err != nil {
|
||||
return err
|
||||
}
|
||||
if record.AudioSize <= 0 {
|
||||
return fmt.Errorf("audio_size must be positive")
|
||||
}
|
||||
if record.Output.Kind != "transcript_raw" {
|
||||
return fmt.Errorf("output kind %q must be transcript_raw", record.Output.Kind)
|
||||
}
|
||||
if err := validateCanonicalRelativePath("output local_path", record.Output.LocalPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateSHA256("output checksum", record.Output.Checksum); err != nil {
|
||||
return err
|
||||
}
|
||||
if record.OutputSize <= 0 {
|
||||
return fmt.Errorf("output_size must be positive")
|
||||
}
|
||||
if err := pathsafe.ValidateOpaqueSegment(record.ProducerRunID); err != nil {
|
||||
return fmt.Errorf("producer_run_id is invalid: %w", err)
|
||||
}
|
||||
if record.Output.ProducerRunID != record.ProducerRunID {
|
||||
return fmt.Errorf("output producer_run_id %q does not match record", record.Output.ProducerRunID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCanonicalRelativePath(field, value string) error {
|
||||
normalized, err := pathsafe.NormalizeRelativeDestination(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s is unsafe: %w", field, err)
|
||||
}
|
||||
if normalized != value {
|
||||
return fmt.Errorf("%s %q is not canonical %q", field, value, normalized)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user