diff --git a/crates/api_clients/fal_client/src/requests/api/audio/mod.rs b/crates/api_clients/fal_client/src/requests/api/audio/mod.rs new file mode 100644 index 0000000000..603cfcb38e --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/audio/mod.rs @@ -0,0 +1 @@ +pub mod omni; diff --git a/crates/api_clients/fal_client/src/requests/api/audio/omni/mod.rs b/crates/api_clients/fal_client/src/requests/api/audio/omni/mod.rs new file mode 100644 index 0000000000..b93956a4ca --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/audio/omni/mod.rs @@ -0,0 +1 @@ +pub mod seed_audio_1p0; diff --git a/crates/api_clients/fal_client/src/requests/api/audio/omni/seed_audio_1p0/api.rs b/crates/api_clients/fal_client/src/requests/api/audio/omni/seed_audio_1p0/api.rs new file mode 100644 index 0000000000..a05c9a14a3 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/audio/omni/seed_audio_1p0/api.rs @@ -0,0 +1,459 @@ +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::api::audio::omni::seed_audio_1p0::raw_request::{ + SeedAudio1p0Input, SeedAudio1p0Output, +}; +use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + +/// ByteDance Seed Audio 1.0: omni audio generation (speech, drama, ambience) +/// with voice presets, voice cloning, and audio/image references. +#[derive(Clone, Debug)] +pub struct SeedAudio1p0Request { + /// Text to synthesize. Reference audio clips supplied via `audio_urls` are + /// cited in the prompt as `@Audio1`, `@Audio2`, `@Audio3`. + pub prompt: String, + + /// Voice to synthesize with: a preset or a cloned voice id. + /// The model picks a voice when `None`. + pub voice: Option, + + /// Up to 3 reference audio URLs, cited in the prompt as `@Audio1`–`@Audio3`. + /// Each clip: ≤30 seconds, ≤10MB, wav/mp3/pcm/ogg_opus. + pub audio_urls: Option>, + + /// A single reference image URL (jpeg/png/webp, ≤10MB) to condition the + /// generation on (e.g. a scene to voice over). + /// NB: fal rejects requests that combine an image reference with audio + /// references — set `image_url` or `audio_urls`, not both. + pub image_url: Option, + + /// Output container. fal's default is mp3 when `None`. + pub output_format: Option, + + /// Output sample rate. fal's default is 24 kHz when `None`. + pub sample_rate: Option, + + /// Speech speed multiplier. Valid range 0.5–2.0; fal's default is 1.0 + /// when `None`. + pub speed: Option, + + /// Volume multiplier. Valid range 0.5–2.0; fal's default is 1.0 when `None`. + pub volume: Option, + + /// Pitch shift in semitones. Valid range −12 to +12; fal's default is 0 + /// when `None`. + pub pitch: Option, +} + +/// A voice for Seed Audio: either one of fal's preset voices or a cloned +/// voice id. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum SeedAudio1p0Voice { + Preset(SeedAudio1p0VoicePreset), + /// The id of a cloned voice. + Custom(String), +} + +impl SeedAudio1p0Voice { + fn to_wire_string(&self) -> String { + match self { + Self::Preset(preset) => preset.to_str().to_string(), + Self::Custom(voice_id) => voice_id.clone(), + } + } +} + +/// fal's preset voices. The suffixes name the languages each voice supports +/// (en = English, zh = Chinese, ja = Japanese, es = Spanish, id = Indonesian, +/// pt = Portuguese). +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum SeedAudio1p0VoicePreset { + ViviMixedEnZhJaEsId, + MindyEnEsIdPtZh, + KianEnZh, + CedricEnZh, + SophieEnZh, + JeanEnZh, + MagnusEnZh, + MabelEnZh, + NadiaEnZh, + OpalEnZh, + PearlEnZh, + QuentinEnZh, + CorinneMixedEnZh, + EstherMixedEnZh, + LylaMixedEnZh, + TracyEsZh, + SandyEsMixedEnZh, + FelixZh, + CelesteZh, + MonkeyKingZh, +} + +impl SeedAudio1p0VoicePreset { + pub fn to_str(&self) -> &'static str { + match self { + Self::ViviMixedEnZhJaEsId => "vivi_mixed_en_zh_ja_es_id", + Self::MindyEnEsIdPtZh => "mindy_en_es_id_pt_zh", + Self::KianEnZh => "kian_en_zh", + Self::CedricEnZh => "cedric_en_zh", + Self::SophieEnZh => "sophie_en_zh", + Self::JeanEnZh => "jean_en_zh", + Self::MagnusEnZh => "magnus_en_zh", + Self::MabelEnZh => "mabel_en_zh", + Self::NadiaEnZh => "nadia_en_zh", + Self::OpalEnZh => "opal_en_zh", + Self::PearlEnZh => "pearl_en_zh", + Self::QuentinEnZh => "quentin_en_zh", + Self::CorinneMixedEnZh => "corinne_mixed_en_zh", + Self::EstherMixedEnZh => "esther_mixed_en_zh", + Self::LylaMixedEnZh => "lyla_mixed_en_zh", + Self::TracyEsZh => "tracy_es_zh", + Self::SandyEsMixedEnZh => "sandy_es_mixed_en_zh", + Self::FelixZh => "felix_zh", + Self::CelesteZh => "celeste_zh", + Self::MonkeyKingZh => "monkey_king_zh", + } + } + + pub fn all() -> [Self; 20] { + [ + Self::ViviMixedEnZhJaEsId, + Self::MindyEnEsIdPtZh, + Self::KianEnZh, + Self::CedricEnZh, + Self::SophieEnZh, + Self::JeanEnZh, + Self::MagnusEnZh, + Self::MabelEnZh, + Self::NadiaEnZh, + Self::OpalEnZh, + Self::PearlEnZh, + Self::QuentinEnZh, + Self::CorinneMixedEnZh, + Self::EstherMixedEnZh, + Self::LylaMixedEnZh, + Self::TracyEsZh, + Self::SandyEsMixedEnZh, + Self::FelixZh, + Self::CelesteZh, + Self::MonkeyKingZh, + ] + } +} + +/// Output audio container. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum SeedAudio1p0OutputFormat { + Wav, + /// fal's default. + Mp3, + Pcm, + OggOpus, +} + +impl SeedAudio1p0OutputFormat { + fn to_str(&self) -> &'static str { + match self { + Self::Wav => "wav", + Self::Mp3 => "mp3", + Self::Pcm => "pcm", + Self::OggOpus => "ogg_opus", + } + } +} + +/// Output sample rate. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum SeedAudio1p0SampleRate { + Hz8000, + Hz16000, + /// fal's default. + Hz24000, + Hz32000, + Hz44100, + Hz48000, +} + +impl SeedAudio1p0SampleRate { + fn as_hz(&self) -> u32 { + match self { + Self::Hz8000 => 8_000, + Self::Hz16000 => 16_000, + Self::Hz24000 => 24_000, + Self::Hz32000 => 32_000, + Self::Hz44100 => 44_100, + Self::Hz48000 => 48_000, + } + } +} + +impl FalEndpoint for SeedAudio1p0Request { + const ENDPOINT: &str = "bytedance/seed-audio-1.0"; + + type RawRequest = SeedAudio1p0Input; + type RawResponse = SeedAudio1p0Output; + + fn to_raw_request(&self) -> Result { + Ok(Self::RawRequest { + prompt: self.prompt.clone(), + voice: self.voice.as_ref().map(|voice| voice.to_wire_string()), + audio_urls: self.audio_urls.clone(), + image_url: self.image_url.clone(), + output_format: self.output_format.map(|format| format.to_str().to_string()), + sample_rate: self.sample_rate.map(|rate| rate.as_hz()), + speed: self.speed, + volume: self.volume, + pitch: self.pitch, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + use errors::AnyhowResult; + use std::fs::read_to_string; + + // ── Real requests (manually run; require a live key and cost money) ── + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn test_seed_audio_queue_minimal() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = SeedAudio1p0Request { + prompt: "Generate a short suspense radio drama in a late-night convenience store.".to_string(), + voice: None, + audio_urls: None, + image_url: None, + output_format: None, + sample_rate: None, + speed: None, + volume: None, + pitch: None, + }; + + let result = request.send_queue_request(&api_key).await?; + println!("Queue result — request_id: {}", result.request_id); + assert!(!result.request_id.is_empty()); + Ok(()) + } + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn test_seed_audio_queue_preset_voice_tuned() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = SeedAudio1p0Request { + prompt: "Welcome aboard the midnight express. Please keep your arms inside the train at all times.".to_string(), + voice: Some(SeedAudio1p0Voice::Preset(SeedAudio1p0VoicePreset::MagnusEnZh)), + audio_urls: None, + image_url: None, + output_format: Some(SeedAudio1p0OutputFormat::Wav), + sample_rate: Some(SeedAudio1p0SampleRate::Hz44100), + speed: Some(0.9), + volume: Some(1.2), + pitch: Some(-2), + }; + + let result = request.send_queue_request(&api_key).await?; + println!("Queue result — request_id: {}", result.request_id); + assert!(!result.request_id.is_empty()); + Ok(()) + } + + #[tokio::test] + #[ignore] // manually run — fires a real API request and incurs cost + async fn test_seed_audio_webhook() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = SeedAudio1p0Request { + prompt: "A calm narrator describes ocean waves rolling onto a moonlit beach.".to_string(), + voice: Some(SeedAudio1p0Voice::Preset(SeedAudio1p0VoicePreset::SophieEnZh)), + audio_urls: None, + image_url: None, + output_format: Some(SeedAudio1p0OutputFormat::Mp3), + sample_rate: None, + speed: None, + volume: None, + pitch: None, + }; + + let result = request.send_webhook_request(&api_key, "https://example.com/webhook").await?; + println!("Webhook result: {:?}", result); + assert!(result.request_id.is_some() || result.gateway_request_id.is_some()); + Ok(()) + } + + // ── Wire-shape sanity (no API calls) ── + + #[test] + fn raw_request_maps_all_fields() { + let request = SeedAudio1p0Request { + prompt: "Read @Audio1 in this voice.".to_string(), + voice: Some(SeedAudio1p0Voice::Preset(SeedAudio1p0VoicePreset::ViviMixedEnZhJaEsId)), + audio_urls: Some(vec![ + "https://example.com/a1.mp3".to_string(), + "https://example.com/a2.wav".to_string(), + "https://example.com/a3.ogg".to_string(), + ]), + image_url: None, + output_format: Some(SeedAudio1p0OutputFormat::OggOpus), + sample_rate: Some(SeedAudio1p0SampleRate::Hz48000), + speed: Some(1.5), + volume: Some(0.5), + pitch: Some(12), + }; + let raw = request.to_raw_request().unwrap(); + assert_eq!(raw.prompt, "Read @Audio1 in this voice."); + assert_eq!(raw.voice.as_deref(), Some("vivi_mixed_en_zh_ja_es_id")); + assert_eq!(raw.audio_urls.as_ref().map(|urls| urls.len()), Some(3)); + assert_eq!(raw.image_url, None); + assert_eq!(raw.output_format.as_deref(), Some("ogg_opus")); + assert_eq!(raw.sample_rate, Some(48_000)); + assert_eq!(raw.speed, Some(1.5)); + assert_eq!(raw.volume, Some(0.5)); + assert_eq!(raw.pitch, Some(12)); + } + + #[test] + fn raw_request_omits_unset_optionals() { + let request = SeedAudio1p0Request { + prompt: "minimal".to_string(), + voice: None, + audio_urls: None, + image_url: None, + output_format: None, + sample_rate: None, + speed: None, + volume: None, + pitch: None, + }; + let json = serde_json::to_value(request.to_raw_request().unwrap()).unwrap(); + assert_eq!(json, serde_json::json!({ "prompt": "minimal" })); + } + + #[test] + fn custom_voice_id_passes_through_verbatim() { + let request = SeedAudio1p0Request { + prompt: "p".to_string(), + voice: Some(SeedAudio1p0Voice::Custom("my_cloned_voice_id_123".to_string())), + audio_urls: None, + image_url: None, + output_format: None, + sample_rate: None, + speed: None, + volume: None, + pitch: None, + }; + let raw = request.to_raw_request().unwrap(); + assert_eq!(raw.voice.as_deref(), Some("my_cloned_voice_id_123")); + } + + #[test] + fn image_url_serializes_when_set() { + let request = SeedAudio1p0Request { + prompt: "Narrate this scene.".to_string(), + voice: None, + audio_urls: None, + image_url: Some("https://example.com/scene.png".to_string()), + output_format: None, + sample_rate: None, + speed: None, + volume: None, + pitch: None, + }; + let json = serde_json::to_value(request.to_raw_request().unwrap()).unwrap(); + assert_eq!( + json, + serde_json::json!({ + "prompt": "Narrate this scene.", + "image_url": "https://example.com/scene.png", + }), + ); + } + + #[test] + fn negative_pitch_serializes() { + let request = SeedAudio1p0Request { + prompt: "p".to_string(), + voice: None, + audio_urls: None, + image_url: None, + output_format: None, + sample_rate: None, + speed: None, + volume: None, + pitch: Some(-12), + }; + let json = serde_json::to_value(request.to_raw_request().unwrap()).unwrap(); + assert_eq!(json.get("pitch").and_then(|p| p.as_i64()), Some(-12)); + } + + #[test] + fn every_voice_preset_maps_to_wire_string() { + let expected = [ + "vivi_mixed_en_zh_ja_es_id", + "mindy_en_es_id_pt_zh", + "kian_en_zh", + "cedric_en_zh", + "sophie_en_zh", + "jean_en_zh", + "magnus_en_zh", + "mabel_en_zh", + "nadia_en_zh", + "opal_en_zh", + "pearl_en_zh", + "quentin_en_zh", + "corinne_mixed_en_zh", + "esther_mixed_en_zh", + "lyla_mixed_en_zh", + "tracy_es_zh", + "sandy_es_mixed_en_zh", + "felix_zh", + "celeste_zh", + "monkey_king_zh", + ]; + for (preset, expected) in SeedAudio1p0VoicePreset::all().iter().zip(expected) { + assert_eq!(preset.to_str(), expected, "wire value for {preset:?}"); + } + } + + #[test] + fn every_output_format_maps_to_wire_string() { + for (variant, expected) in [ + (SeedAudio1p0OutputFormat::Wav, "wav"), + (SeedAudio1p0OutputFormat::Mp3, "mp3"), + (SeedAudio1p0OutputFormat::Pcm, "pcm"), + (SeedAudio1p0OutputFormat::OggOpus, "ogg_opus"), + ] { + assert_eq!(variant.to_str(), expected); + } + } + + #[test] + fn every_sample_rate_maps_to_hz() { + for (variant, expected) in [ + (SeedAudio1p0SampleRate::Hz8000, 8_000), + (SeedAudio1p0SampleRate::Hz16000, 16_000), + (SeedAudio1p0SampleRate::Hz24000, 24_000), + (SeedAudio1p0SampleRate::Hz32000, 32_000), + (SeedAudio1p0SampleRate::Hz44100, 44_100), + (SeedAudio1p0SampleRate::Hz48000, 48_000), + ] { + assert_eq!(variant.as_hz(), expected); + } + } + + #[test] + fn endpoint_path_is_canonical() { + assert_eq!(SeedAudio1p0Request::ENDPOINT, "bytedance/seed-audio-1.0"); + } + + // NB: Pricing tests are in cost.rs +} diff --git a/crates/api_clients/fal_client/src/requests/api/audio/omni/seed_audio_1p0/cost.rs b/crates/api_clients/fal_client/src/requests/api/audio/omni/seed_audio_1p0/cost.rs new file mode 100644 index 0000000000..1213a3dbbb --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/audio/omni/seed_audio_1p0/cost.rs @@ -0,0 +1,128 @@ +use crate::requests::api::audio::omni::seed_audio_1p0::api::SeedAudio1p0Request; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; + +// Seed Audio 1.0 pricing (see https://fal.ai/models/bytedance/seed-audio-1.0): +// "Your request will cost $0.1875 per minute." +// +// fal bills per minute of GENERATED audio, and no request parameter fixes the +// output duration — it depends on the prompt. The rate is stored in +// hundredths-of-a-cent per minute (exact), and per-duration costs are rounded +// UP to whole cents so the user is never undercharged. + +/// $0.1875 per minute, in hundredths of a cent. +pub const SEED_AUDIO_1P0_RATE_HUNDREDTH_CENTS_PER_MINUTE: u64 = 1_875; + +/// The duration assumed by the parameterless [`FalRequestCostCalculator`] +/// estimate (the output length isn't knowable at request time). +pub const SEED_AUDIO_1P0_ESTIMATED_DURATION_SECONDS: u64 = 60; + +/// Cost in whole cents for a known output duration: +/// ceil(1875 hundredth-cents/min × seconds / 60 / 100). +pub fn seed_audio_1p0_cost_cents_for_duration_seconds(duration_seconds: u64) -> UsdCents { + (SEED_AUDIO_1P0_RATE_HUNDREDTH_CENTS_PER_MINUTE * duration_seconds).div_ceil(6_000) +} + +impl FalRequestCostCalculator for SeedAudio1p0Request { + /// The output duration isn't known until the audio is generated, so this + /// estimates one minute of output. Use + /// [`seed_audio_1p0_cost_cents_for_duration_seconds`] to price the actual + /// generated duration once it is known. + fn calculate_cost_in_cents(&self) -> UsdCents { + seed_audio_1p0_cost_cents_for_duration_seconds(SEED_AUDIO_1P0_ESTIMATED_DURATION_SECONDS) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod duration_cost_table { + use super::*; + + // (duration_seconds, expected_cents) + // Math: ceil($0.1875/min × seconds), i.e. ceil(18.75¢ × secs / 60). + const COST_TABLE: &[(u64, u64)] = &[ + (0, 0), + (1, 1), // 0.3125 → 1 + (10, 4), // 3.125 → 4 + (30, 10), // 9.375 → 10 + (32, 10), // exactly 10.0 + (60, 19), // 18.75 → 19 + (61, 20), // 19.0625 → 20 + (90, 29), // 28.125 → 29 + (96, 30), // exactly 30.0 + (120, 38), // 37.5 → 38 + (300, 94), // 93.75 → 94 + (600, 188), // 187.5 → 188 + ]; + + #[test] + fn matches_cost_table() { + for &(duration_seconds, expected) in COST_TABLE { + let got = seed_audio_1p0_cost_cents_for_duration_seconds(duration_seconds); + assert_eq!(got, expected, "duration_seconds={duration_seconds}"); + } + } + + #[test] + fn cost_scales_with_duration() { + let c30 = seed_audio_1p0_cost_cents_for_duration_seconds(30); + let c60 = seed_audio_1p0_cost_cents_for_duration_seconds(60); + let c120 = seed_audio_1p0_cost_cents_for_duration_seconds(120); + assert!(c30 < c60); + assert!(c60 < c120); + } + + #[test] + fn exact_minute_multiples_never_round() { + // 16 minutes = exactly 300¢ ($3.00): 1875 × 960 / 6000 = 300. + assert_eq!(seed_audio_1p0_cost_cents_for_duration_seconds(960), 300); + } + } + + mod request_estimate { + use super::*; + + fn make_request() -> SeedAudio1p0Request { + SeedAudio1p0Request { + prompt: "test".to_string(), + voice: None, + audio_urls: None, + image_url: None, + output_format: None, + sample_rate: None, + speed: None, + volume: None, + pitch: None, + } + } + + #[test] + fn estimates_one_minute() { + // 60s at $0.1875/min = 18.75¢ → rounds up to 19¢. + assert_eq!(make_request().calculate_cost_in_cents(), 19); + assert_eq!( + make_request().calculate_cost_in_cents(), + seed_audio_1p0_cost_cents_for_duration_seconds(SEED_AUDIO_1P0_ESTIMATED_DURATION_SECONDS), + ); + } + + #[test] + fn request_options_do_not_affect_the_estimate() { + use crate::requests::api::audio::omni::seed_audio_1p0::api::{ + SeedAudio1p0OutputFormat, SeedAudio1p0SampleRate, SeedAudio1p0Voice, + SeedAudio1p0VoicePreset, + }; + + let mut tuned = make_request(); + tuned.voice = Some(SeedAudio1p0Voice::Preset(SeedAudio1p0VoicePreset::FelixZh)); + tuned.output_format = Some(SeedAudio1p0OutputFormat::Wav); + tuned.sample_rate = Some(SeedAudio1p0SampleRate::Hz48000); + tuned.speed = Some(2.0); + tuned.volume = Some(0.5); + tuned.pitch = Some(-12); + + assert_eq!(tuned.calculate_cost_in_cents(), make_request().calculate_cost_in_cents()); + } + } +} diff --git a/crates/api_clients/fal_client/src/requests/api/audio/omni/seed_audio_1p0/mod.rs b/crates/api_clients/fal_client/src/requests/api/audio/omni/seed_audio_1p0/mod.rs new file mode 100644 index 0000000000..3bfb6c86db --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/audio/omni/seed_audio_1p0/mod.rs @@ -0,0 +1,3 @@ +pub mod api; +pub mod cost; +pub mod raw_request; diff --git a/crates/api_clients/fal_client/src/requests/api/audio/omni/seed_audio_1p0/raw_request.rs b/crates/api_clients/fal_client/src/requests/api/audio/omni/seed_audio_1p0/raw_request.rs new file mode 100644 index 0000000000..e763c50b2f --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/audio/omni/seed_audio_1p0/raw_request.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; + +/// Over-the-wire input shape for `bytedance/seed-audio-1.0`. +/// fal's schema: +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct SeedAudio1p0Input { + /// Text to synthesize. Reference audio clips are cited in the prompt as + /// `@Audio1`, `@Audio2`, `@Audio3`. + pub prompt: String, + + /// Voice to use: a preset voice name or a cloned voice id. + #[serde(skip_serializing_if = "Option::is_none")] + pub voice: Option, + + /// Up to 3 reference audio URLs (each ≤30s and ≤10MB; wav/mp3/pcm/ogg_opus). + #[serde(skip_serializing_if = "Option::is_none")] + pub audio_urls: Option>, + + /// A single reference image URL (jpeg/png/webp, ≤10MB). + /// Cannot be combined with audio references. + #[serde(skip_serializing_if = "Option::is_none")] + pub image_url: Option, + + /// Output container. + /// Possible values: "wav", "mp3", "pcm", "ogg_opus". fal default: "mp3". + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, + + /// Output sample rate in Hz. + /// Possible values: 8000, 16000, 24000, 32000, 44100, 48000. fal default: 24000. + #[serde(skip_serializing_if = "Option::is_none")] + pub sample_rate: Option, + + /// Speech speed multiplier. Range 0.5–2.0. fal default: 1.0. + #[serde(skip_serializing_if = "Option::is_none")] + pub speed: Option, + + /// Volume multiplier. Range 0.5–2.0. fal default: 1.0. + #[serde(skip_serializing_if = "Option::is_none")] + pub volume: Option, + + /// Pitch shift in semitones. Range −12 to +12. fal default: 0. + #[serde(skip_serializing_if = "Option::is_none")] + pub pitch: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SeedAudio1p0AudioFile { + pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub content_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub file_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub file_size: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SeedAudio1p0Output { + pub audio: SeedAudio1p0AudioFile, +} diff --git a/crates/api_clients/fal_client/src/requests/api/mod.rs b/crates/api_clients/fal_client/src/requests/api/mod.rs index 9c42e24f90..f3fac572fa 100644 --- a/crates/api_clients/fal_client/src/requests/api/mod.rs +++ b/crates/api_clients/fal_client/src/requests/api/mod.rs @@ -1,3 +1,4 @@ +pub mod audio; pub mod image; pub mod mesh; pub mod video; \ No newline at end of file