use std::num::{NonZeroU64, NonZeroUsize};
use serde::Serialize;
use crate::error::{Error, Result};
use crate::latent::KindTag;
use crate::manifest::SourceKind;
use crate::seams::{GuidanceScale, Temperature};
use crate::time::SampleRate;
use super::raw::{
Alignment, RawAudioCodec, RawDataEncoder, RawExperimentConfig, RawGenerator, RawSegmentation,
RawSourceData, RawTargetAudio, RawTraining,
};
impl RawExperimentConfig {
pub fn resolve(self) -> Result<ExperimentSpec> {
require_non_empty(&self.project, "project")?;
require_non_empty(&self.experiment_name, "experiment_name")?;
let sample_rate = SampleRate::try_from(self.sample_rate)?;
let clip_duration_seconds =
require_positive_finite(self.clip_duration_seconds, "clip_duration_seconds")?;
let source_data = self.source_data.resolve()?;
let target_audio = self.target_audio.resolve(sample_rate)?;
let data_encoder = self.data_encoder.resolve()?;
let audio_codec = self.audio_codec.resolve()?;
let generator = self.conditional_generator.resolve()?;
let training = self.training.resolve()?;
let generation = GenerationDefaults::resolve(&self.generation)?;
Ok(ExperimentSpec {
sample_rate,
clip_duration_seconds,
source_data,
target_audio,
data_encoder,
audio_codec,
generator,
training,
generation,
raw: self,
})
}
}
#[derive(Clone, Debug)]
pub struct ExperimentSpec {
sample_rate: SampleRate,
clip_duration_seconds: f32,
source_data: SourceDataSpec,
target_audio: TargetAudioSpec,
data_encoder: DataEncoderSpec,
audio_codec: AudioCodecSpec,
generator: GeneratorSpec,
training: TrainingSpec,
generation: GenerationDefaults,
raw: RawExperimentConfig,
}
impl ExperimentSpec {
pub fn project(&self) -> &str {
&self.raw.project
}
pub fn experiment_name(&self) -> &str {
&self.raw.experiment_name
}
pub fn sample_rate(&self) -> SampleRate {
self.sample_rate
}
pub fn clip_duration_seconds(&self) -> f32 {
self.clip_duration_seconds
}
pub fn source_data(&self) -> &SourceDataSpec {
&self.source_data
}
pub fn target_audio(&self) -> &TargetAudioSpec {
&self.target_audio
}
pub fn data_encoder(&self) -> &DataEncoderSpec {
&self.data_encoder
}
pub fn audio_codec(&self) -> &AudioCodecSpec {
&self.audio_codec
}
pub fn generator(&self) -> &GeneratorSpec {
&self.generator
}
pub fn training(&self) -> &TrainingSpec {
&self.training
}
pub fn generation(&self) -> &GenerationDefaults {
&self.generation
}
pub fn recipe(&self) -> super::Recipe<'_> {
super::Recipe::new(&self.raw)
}
pub fn config_fingerprint(&self) -> Result<super::ConfigFingerprint> {
super::ConfigFingerprint::of(&self.raw)
}
pub fn recipe_fingerprint(&self) -> Result<super::RecipeFingerprint> {
super::RecipeFingerprint::of(&self.recipe())
}
}
impl RawSourceData {
fn resolve(&self) -> Result<SourceDataSpec> {
require_non_empty(&self.path, "source_data.path")?;
Ok(SourceDataSpec {
kind: self.kind,
path: self.path.clone(),
alignment: self.alignment,
})
}
}
#[derive(Clone, Debug)]
pub struct SourceDataSpec {
kind: SourceKind,
path: String,
alignment: Alignment,
}
impl SourceDataSpec {
pub fn kind(&self) -> SourceKind {
self.kind
}
pub fn path(&self) -> &str {
&self.path
}
pub fn alignment(&self) -> Alignment {
self.alignment
}
}
impl RawTargetAudio {
fn resolve(&self, sample_rate: SampleRate) -> Result<TargetAudioSpec> {
require_non_empty(&self.path, "target_audio.path")?;
let segmentation = Segmentation::resolve(&self.segmentation, sample_rate)?;
Ok(TargetAudioSpec {
path: self.path.clone(),
segmentation,
})
}
}
#[derive(Clone, Debug)]
pub struct TargetAudioSpec {
path: String,
segmentation: Segmentation,
}
impl TargetAudioSpec {
pub fn path(&self) -> &str {
&self.path
}
pub fn segmentation(&self) -> &Segmentation {
&self.segmentation
}
}
#[derive(Clone, Copy, Debug)]
pub struct Segmentation {
window_seconds: f32,
overlap_seconds: f32,
window_frames: NonZeroU64,
overlap_frames: u64,
}
impl Segmentation {
fn resolve(raw: &RawSegmentation, sample_rate: SampleRate) -> Result<Self> {
let window_seconds =
require_positive_finite(raw.window_seconds, "segmentation.window_seconds")?;
let overlap_seconds =
require_non_negative_finite(raw.overlap_seconds, "segmentation.overlap_seconds")?;
if overlap_seconds >= window_seconds {
return Err(Error::validation(
"segmentation.overlap_seconds must be smaller than window_seconds",
));
}
let sr = sample_rate.get() as f32;
let window_frames = (window_seconds * sr).round() as u64;
let overlap_frames = (overlap_seconds * sr).round() as u64;
let window_frames = NonZeroU64::new(window_frames).ok_or_else(|| {
Error::validation(
"segmentation.window_seconds is too small for the sample rate (rounds to zero frames)",
)
})?;
if overlap_frames >= window_frames.get() {
return Err(Error::validation(
"segmentation.overlap_seconds rounds to at least a full window at this sample rate",
));
}
Ok(Self {
window_seconds,
overlap_seconds,
window_frames,
overlap_frames,
})
}
pub fn window_seconds(&self) -> f32 {
self.window_seconds
}
pub fn overlap_seconds(&self) -> f32 {
self.overlap_seconds
}
pub fn window_frames(&self) -> NonZeroU64 {
self.window_frames
}
pub fn overlap_frames(&self) -> u64 {
self.overlap_frames
}
}
impl RawDataEncoder {
fn resolve(&self) -> Result<DataEncoderSpec> {
require_non_empty(&self.kind, "data_encoder.type")?;
let latent_dim = NonZeroUsize::new(self.latent_dim).ok_or_else(|| {
Error::validation("data_encoder.latent_dim must be greater than zero")
})?;
Ok(DataEncoderSpec {
kind: self.kind.clone(),
latent_dim,
extra: self.extra.clone(),
})
}
}
#[derive(Clone, Debug)]
pub struct DataEncoderSpec {
kind: String,
latent_dim: NonZeroUsize,
extra: crate::latent::Metadata,
}
impl DataEncoderSpec {
pub fn kind(&self) -> &str {
&self.kind
}
pub fn latent_dim(&self) -> NonZeroUsize {
self.latent_dim
}
pub fn extra(&self) -> &crate::latent::Metadata {
&self.extra
}
}
impl RawAudioCodec {
fn resolve(&self) -> Result<AudioCodecSpec> {
require_non_empty(&self.kind, "audio_codec.type")?;
Ok(AudioCodecSpec {
kind: self.kind.clone(),
latent_kind: self.latent_type,
freeze: self.freeze,
extra: self.extra.clone(),
})
}
}
#[derive(Clone, Debug)]
pub struct AudioCodecSpec {
kind: String,
latent_kind: KindTag,
freeze: bool,
extra: crate::latent::Metadata,
}
impl AudioCodecSpec {
pub fn kind(&self) -> &str {
&self.kind
}
pub fn latent_kind(&self) -> KindTag {
self.latent_kind
}
pub fn freeze(&self) -> bool {
self.freeze
}
pub fn extra(&self) -> &crate::latent::Metadata {
&self.extra
}
}
impl RawGenerator {
fn resolve(&self) -> Result<GeneratorSpec> {
require_non_empty(&self.kind, "conditional_generator.type")?;
Ok(GeneratorSpec {
kind: self.kind.clone(),
extra: self.extra.clone(),
})
}
}
#[derive(Clone, Debug)]
pub struct GeneratorSpec {
kind: String,
extra: crate::latent::Metadata,
}
impl GeneratorSpec {
pub fn kind(&self) -> &str {
&self.kind
}
pub fn extra(&self) -> &crate::latent::Metadata {
&self.extra
}
}
impl RawTraining {
fn resolve(&self) -> Result<TrainingSpec> {
let batch_size = NonZeroUsize::new(self.batch_size)
.ok_or_else(|| Error::validation("training.batch_size must be greater than zero"))?;
let max_steps = NonZeroU64::new(self.max_steps)
.ok_or_else(|| Error::validation("training.max_steps must be greater than zero"))?;
if !self.learning_rate.is_finite() || self.learning_rate <= 0.0 {
return Err(Error::validation(format!(
"training.learning_rate must be finite and positive, got {}",
self.learning_rate
)));
}
Ok(TrainingSpec {
batch_size,
learning_rate: self.learning_rate,
max_steps,
seed: self.seed,
})
}
}
#[derive(Clone, Debug)]
pub struct TrainingSpec {
batch_size: NonZeroUsize,
learning_rate: f64,
max_steps: NonZeroU64,
seed: u64,
}
impl TrainingSpec {
pub fn batch_size(&self) -> NonZeroUsize {
self.batch_size
}
pub fn learning_rate(&self) -> f64 {
self.learning_rate
}
pub fn max_steps(&self) -> NonZeroU64 {
self.max_steps
}
pub fn seed(&self) -> u64 {
self.seed
}
}
#[derive(Clone, Copy, Debug)]
pub struct GenerationDefaults {
temperature: Option<Temperature>,
guidance: GuidanceScale,
candidates: NonZeroUsize,
}
impl GenerationDefaults {
fn resolve(raw: &super::raw::RawGeneration) -> Result<Self> {
let temperature = if raw.temperature == 0.0 {
None
} else if !raw.temperature.is_finite() || raw.temperature < 0.0 {
return Err(Error::validation(format!(
"generation.temperature must be finite and non-negative, got {}",
raw.temperature
)));
} else {
Some(Temperature::new(raw.temperature)?)
};
let guidance = GuidanceScale::new(raw.guidance_scale).map_err(|_| {
Error::validation(format!(
"generation.guidance_scale must be finite and non-negative, got {}",
raw.guidance_scale
))
})?;
let candidates = usize::try_from(raw.num_candidates)
.ok()
.and_then(NonZeroUsize::new)
.ok_or_else(|| Error::validation("generation.num_candidates must be at least one"))?;
Ok(Self {
temperature,
guidance,
candidates,
})
}
pub fn temperature(&self) -> Option<Temperature> {
self.temperature
}
pub fn guidance(&self) -> GuidanceScale {
self.guidance
}
pub fn candidates(&self) -> NonZeroUsize {
self.candidates
}
}
fn require_non_empty(value: &str, field: &str) -> Result<()> {
if value.trim().is_empty() {
return Err(Error::validation(format!("{field} must not be empty")));
}
Ok(())
}
fn require_positive_finite(value: f32, field: &str) -> Result<f32> {
if !value.is_finite() || value <= 0.0 {
return Err(Error::validation(format!(
"{field} must be finite and positive, got {value}"
)));
}
Ok(value)
}
fn require_non_negative_finite(value: f32, field: &str) -> Result<f32> {
if !value.is_finite() || value < 0.0 {
return Err(Error::validation(format!(
"{field} must be finite and non-negative, got {value}"
)));
}
Ok(value)
}
#[derive(Debug, Serialize)]
pub struct Recipe<'a> {
project: &'a str,
experiment_name: &'a str,
sample_rate: u32,
clip_duration_seconds: f32,
source_data: &'a RawSourceData,
target_audio: &'a RawTargetAudio,
data_encoder: &'a RawDataEncoder,
audio_codec: &'a RawAudioCodec,
conditional_generator: &'a RawGenerator,
training: &'a RawTraining,
}
impl<'a> Recipe<'a> {
pub(super) fn new(raw: &'a RawExperimentConfig) -> Self {
Self {
project: &raw.project,
experiment_name: &raw.experiment_name,
sample_rate: raw.sample_rate,
clip_duration_seconds: raw.clip_duration_seconds,
source_data: &raw.source_data,
target_audio: &raw.target_audio,
data_encoder: &raw.data_encoder,
audio_codec: &raw.audio_codec,
conditional_generator: &raw.conditional_generator,
training: &raw.training,
}
}
}