helena 0.1.0

Core types and component interfaces for helena, a latent data-to-waveform generation platform.
Documentation
//! Stamped generated-artifact metadata (PRD §18.2, NFR-010).
//!
//! Every generated artifact ties back to the exact inputs that produced it.
//! The old design assembled that record field-by-field at call sites — a
//! provenance hole per forgotten field. Here the record is constructed only
//! by [`GeneratedArtifact::stamp`], whose input demands every NFR-010 fact
//! as a *typed* value: fingerprints that can only be minted from the real
//! config and source ([`ConfigFingerprint`], [`SourceFingerprint`]), a codec
//! description carrying a [`KindTag`] rather than a free string, and the
//! generation record's [`SeedPolicy`]. The `synthetic` label (NFR-040) is
//! not a settable field: it is a fact of the type, enforced on
//! deserialization too.

use serde::{Deserialize, Serialize};

use crate::config::ConfigFingerprint;
use crate::diffusion::GuidanceSchedule;
use crate::error::{Error, Result};
use crate::latent::KindTag;
use crate::provenance::SourceFingerprint;
use crate::seams::{ClipDuration, GenerationParams, GuidanceScale, SeedPolicy};
use crate::time::SampleRate;

/// The artifact-type discriminator this module writes. A record claiming a
/// different type fails deserialization here — other artifact families get
/// their own types, not a shared stringly field.
const ARTIFACT_TYPE: &str = "generated_audio";

/// The codec that decoded the artifact's latent (PRD §18.2 `audio_codec`).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CodecInfo {
    /// Backend name (e.g. `"pcm"`, `"vae"`).
    pub name: String,
    /// Backend version or checkpoint identity.
    pub version: String,
    /// The codec's operating sample rate.
    pub sample_rate: SampleRate,
    /// The latent kind the codec speaks.
    pub latent_kind: KindTag,
}

/// The generation call, recorded losslessly.
///
/// The old record projected back into call parameters *lossily* (the
/// guidance schedule was silently dropped). Here [`params`](Self::params)
/// and [`guidance_schedule`](Self::guidance_schedule) together reconstruct
/// the call exactly.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct GenerationRecord {
    duration: ClipDuration,
    seed: SeedPolicy,
    guidance: GuidanceScale,
    /// Defaults to constant for records written before the schedule existed.
    #[serde(default)]
    guidance_schedule: GuidanceSchedule,
}

impl GenerationRecord {
    /// Record a generation call.
    pub fn new(params: &GenerationParams, guidance_schedule: GuidanceSchedule) -> Self {
        Self {
            duration: params.duration,
            seed: params.seed,
            guidance: params.guidance,
            guidance_schedule,
        }
    }

    /// The per-call parameters, reconstructed exactly.
    pub fn params(&self) -> GenerationParams {
        GenerationParams {
            duration: self.duration,
            seed: self.seed,
            guidance: self.guidance,
        }
    }

    /// The guidance profile the call ran under.
    pub fn guidance_schedule(&self) -> &GuidanceSchedule {
        &self.guidance_schedule
    }

    /// Whether the recorded call promises bit-identical replay (NFR-012).
    pub fn replayable(&self) -> bool {
        self.seed.replayable()
    }
}

/// Everything NFR-010 demands, as one named-field input to
/// [`GeneratedArtifact::stamp`]. A hole in provenance is a missing field —
/// a compile error at the call site, not a blank in a report.
#[derive(Clone, Debug)]
pub struct Stamp {
    /// Project name.
    pub project: String,
    /// The run that produced the model.
    pub run_id: String,
    /// The source item generated from.
    pub source_item_id: String,
    /// The checkpoint file the generator loaded.
    pub model_checkpoint: String,
    /// Fingerprint of the experiment config (mintable only from the real
    /// raw config).
    pub config: ConfigFingerprint,
    /// Fingerprint of the source batch (mintable only from the real batch).
    pub source: SourceFingerprint,
    /// The decoding codec.
    pub codec: CodecInfo,
    /// The generation call.
    pub generation: GenerationRecord,
}

/// A generated artifact's provenance record (PRD §18.2).
///
/// Constructed only by [`stamp`](Self::stamp); the `synthetic: true` label
/// (NFR-040) and the artifact type are facts of the type, re-proven on
/// deserialization — a hand-edited record claiming `synthetic: false` fails
/// closed.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "ArtifactRepr", into = "ArtifactRepr")]
pub struct GeneratedArtifact {
    project: String,
    run_id: String,
    source_item_id: String,
    model_checkpoint: String,
    config: ConfigFingerprint,
    source: SourceFingerprint,
    codec: CodecInfo,
    generation: GenerationRecord,
}

impl GeneratedArtifact {
    /// Stamp an artifact with its complete provenance.
    pub fn stamp(stamp: Stamp) -> Self {
        Self {
            project: stamp.project,
            run_id: stamp.run_id,
            source_item_id: stamp.source_item_id,
            model_checkpoint: stamp.model_checkpoint,
            config: stamp.config,
            source: stamp.source,
            codec: stamp.codec,
            generation: stamp.generation,
        }
    }

    /// Project name.
    pub fn project(&self) -> &str {
        &self.project
    }

    /// The run that produced the model.
    pub fn run_id(&self) -> &str {
        &self.run_id
    }

    /// The source item generated from.
    pub fn source_item_id(&self) -> &str {
        &self.source_item_id
    }

    /// The checkpoint file the generator loaded.
    pub fn model_checkpoint(&self) -> &str {
        &self.model_checkpoint
    }

    /// Fingerprint of the experiment config.
    pub fn config(&self) -> ConfigFingerprint {
        self.config
    }

    /// Fingerprint of the source batch.
    pub fn source(&self) -> SourceFingerprint {
        self.source
    }

    /// The decoding codec.
    pub fn codec(&self) -> &CodecInfo {
        &self.codec
    }

    /// The generation call.
    pub fn generation(&self) -> &GenerationRecord {
        &self.generation
    }
}

/// Wire form. Carries the constant `artifact_type` and `synthetic` fields
/// explicitly (so exported JSON is self-describing, PRD §18.2) and re-proves
/// them on the way back in.
#[derive(Serialize, Deserialize)]
struct ArtifactRepr {
    artifact_type: String,
    project: String,
    run_id: String,
    source_item_id: String,
    model_checkpoint: String,
    config: ConfigFingerprint,
    source: SourceFingerprint,
    codec: CodecInfo,
    generation: GenerationRecord,
    synthetic: bool,
}

impl From<GeneratedArtifact> for ArtifactRepr {
    fn from(artifact: GeneratedArtifact) -> Self {
        Self {
            artifact_type: ARTIFACT_TYPE.to_owned(),
            project: artifact.project,
            run_id: artifact.run_id,
            source_item_id: artifact.source_item_id,
            model_checkpoint: artifact.model_checkpoint,
            config: artifact.config,
            source: artifact.source,
            codec: artifact.codec,
            generation: artifact.generation,
            synthetic: true,
        }
    }
}

impl TryFrom<ArtifactRepr> for GeneratedArtifact {
    type Error = Error;

    fn try_from(repr: ArtifactRepr) -> Result<Self> {
        if repr.artifact_type != ARTIFACT_TYPE {
            return Err(Error::validation(format!(
                "artifact type {:?} is not {ARTIFACT_TYPE:?}",
                repr.artifact_type
            )));
        }
        if !repr.synthetic {
            return Err(Error::validation(
                "generated artifacts are synthetic by definition (NFR-040); \
                 a record claiming otherwise is corrupt",
            ));
        }
        Ok(Self {
            project: repr.project,
            run_id: repr.run_id,
            source_item_id: repr.source_item_id,
            model_checkpoint: repr.model_checkpoint,
            config: repr.config,
            source: repr.source,
            codec: repr.codec,
            generation: repr.generation,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::RawExperimentConfig;
    use crate::seams::{Seed, SourceBatch, Temperature};
    use crate::tensor::Tensor;

    fn stamp() -> Stamp {
        let raw = RawExperimentConfig::from_json_str(
            &serde_json::to_string(&serde_json::json!({
                "project": "helena",
                "experiment_name": "t",
                "sample_rate": 8000,
                "clip_duration_seconds": 1.0,
                "source_data": { "type": "tabular", "path": "src" },
                "target_audio": { "path": "audio" },
                "data_encoder": { "type": "vector", "latent_dim": 2 },
                "audio_codec": { "type": "pcm", "latent_type": "continuous" },
                "conditional_generator": { "type": "mlp" },
                "training": { "batch_size": 1, "learning_rate": 0.001, "max_steps": 1, "seed": 0 }
            }))
            .unwrap(),
        )
        .unwrap();
        let params = GenerationParams {
            duration: ClipDuration::new(2.5).unwrap(),
            seed: SeedPolicy::Seeded {
                seed: Seed(123),
                temperature: Temperature::new(0.8).unwrap(),
            },
            guidance: GuidanceScale::new(1.5).unwrap(),
        };
        Stamp {
            project: "helena".into(),
            run_id: "run-1".into(),
            source_item_id: "item_0001".into(),
            model_checkpoint: "checkpoint_step_10.helw".into(),
            config: ConfigFingerprint::of(&raw).unwrap(),
            source: SourceFingerprint::of(&SourceBatch::new(vec![Tensor::vector([1.0])])).unwrap(),
            codec: CodecInfo {
                name: "pcm".into(),
                version: "1".into(),
                sample_rate: SampleRate::try_from(8_000).unwrap(),
                latent_kind: KindTag::Continuous,
            },
            generation: GenerationRecord::new(&params, GuidanceSchedule::default()),
        }
    }

    #[test]
    fn stamp_round_trips_and_reconstructs_the_call() {
        let artifact = GeneratedArtifact::stamp(stamp());
        let json = serde_json::to_string(&artifact).unwrap();
        assert!(json.contains(r#""synthetic":true"#));
        assert!(json.contains(r#""artifact_type":"generated_audio""#));

        let back: GeneratedArtifact = serde_json::from_str(&json).unwrap();
        assert_eq!(back, artifact);

        // Lossless call reconstruction (the old record dropped the schedule).
        let params = back.generation().params();
        assert_eq!(params.seed.seed(), Some(Seed(123)));
        assert!(back.generation().replayable());
    }

    #[test]
    fn deserialization_re_proves_the_synthetic_label() {
        let json = serde_json::to_string(&GeneratedArtifact::stamp(stamp())).unwrap();
        let unlabeled = json.replace(r#""synthetic":true"#, r#""synthetic":false"#);
        assert!(serde_json::from_str::<GeneratedArtifact>(&unlabeled).is_err());
        let wrong_type = json.replace("generated_audio", "handwritten_audio");
        assert!(serde_json::from_str::<GeneratedArtifact>(&wrong_type).is_err());
    }
}