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;
const ARTIFACT_TYPE: &str = "generated_audio";
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CodecInfo {
pub name: String,
pub version: String,
pub sample_rate: SampleRate,
pub latent_kind: KindTag,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct GenerationRecord {
duration: ClipDuration,
seed: SeedPolicy,
guidance: GuidanceScale,
#[serde(default)]
guidance_schedule: GuidanceSchedule,
}
impl GenerationRecord {
pub fn new(params: &GenerationParams, guidance_schedule: GuidanceSchedule) -> Self {
Self {
duration: params.duration,
seed: params.seed,
guidance: params.guidance,
guidance_schedule,
}
}
pub fn params(&self) -> GenerationParams {
GenerationParams {
duration: self.duration,
seed: self.seed,
guidance: self.guidance,
}
}
pub fn guidance_schedule(&self) -> &GuidanceSchedule {
&self.guidance_schedule
}
pub fn replayable(&self) -> bool {
self.seed.replayable()
}
}
#[derive(Clone, Debug)]
pub struct Stamp {
pub project: String,
pub run_id: String,
pub source_item_id: String,
pub model_checkpoint: String,
pub config: ConfigFingerprint,
pub source: SourceFingerprint,
pub codec: CodecInfo,
pub generation: GenerationRecord,
}
#[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 {
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,
}
}
pub fn project(&self) -> &str {
&self.project
}
pub fn run_id(&self) -> &str {
&self.run_id
}
pub fn source_item_id(&self) -> &str {
&self.source_item_id
}
pub fn model_checkpoint(&self) -> &str {
&self.model_checkpoint
}
pub fn config(&self) -> ConfigFingerprint {
self.config
}
pub fn source(&self) -> SourceFingerprint {
self.source
}
pub fn codec(&self) -> &CodecInfo {
&self.codec
}
pub fn generation(&self) -> &GenerationRecord {
&self.generation
}
}
#[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(¶ms, 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);
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());
}
}