129 lines
4.6 KiB
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
|
|
}
|