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 }