Files
narratio/internal/manifest/transcribe_state.go

129 lines
4.6 KiB
Go

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 not be negative")
}
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
}