Compare commits

...
Author SHA1 Message Date
Brandon Thomas f8f6022b33 fix 2026-07-07 01:26:02 -04:00
Brandon Thomas 2d498e20a1 veo 4k 2026-07-07 00:55:20 -04:00
10 changed files with 136 additions and 60 deletions
@@ -15,9 +15,9 @@ impl ArtcraftVeo2CostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Mirrors fal_client veo_2: 5s = $2.50, +$0.50/s above 5s.
// 5s = $2.75, +$0.55/s above 5s.
let extra = self.duration_seconds.saturating_sub(5);
let cost_in_usd_cents = 250 + extra * 50;
let cost_in_usd_cents = 275 + extra * 55;
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -49,17 +49,17 @@ mod tests {
}
#[test]
fn default_is_5s_priced_at_250() { assert_eq!(cost_cents(None), 250); }
fn default_is_5s_priced_at_275() { assert_eq!(cost_cents(None), 275); }
#[test]
fn five_seconds_is_250() { assert_eq!(cost_cents(Some(5)), 250); }
fn five_seconds_is_275() { assert_eq!(cost_cents(Some(5)), 275); }
#[test]
fn six_seconds_is_300() { assert_eq!(cost_cents(Some(6)), 300); }
fn six_seconds_is_330() { assert_eq!(cost_cents(Some(6)), 330); }
#[test]
fn seven_seconds_is_350() { assert_eq!(cost_cents(Some(7)), 350); }
fn seven_seconds_is_385() { assert_eq!(cost_cents(Some(7)), 385); }
#[test]
fn eight_seconds_is_400() { assert_eq!(cost_cents(Some(8)), 400); }
fn eight_seconds_is_440() { assert_eq!(cost_cents(Some(8)), 440); }
}
@@ -17,8 +17,8 @@ impl ArtcraftVeo3CostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Mirrors fal_client veo_3: $0.20/sec audio off, $0.40/sec audio on.
let per_second_cents: u64 = if self.generate_audio { 40 } else { 20 };
// $0.24/sec audio off, $0.48/sec audio on.
let per_second_cents: u64 = if self.generate_audio { 48 } else { 24 };
let cost_in_usd_cents = per_second_cents * self.duration_seconds;
VideoGenerationCostEstimate {
@@ -61,19 +61,19 @@ mod tests {
}
#[test]
fn audio_off_4s_is_80() { assert_eq!(cost_cents(Some(4), Some(false)), 80); }
fn audio_off_4s_is_96() { assert_eq!(cost_cents(Some(4), Some(false)), 96); }
#[test]
fn audio_off_6s_is_120() { assert_eq!(cost_cents(Some(6), Some(false)), 120); }
fn audio_off_6s_is_144() { assert_eq!(cost_cents(Some(6), Some(false)), 144); }
#[test]
fn audio_off_8s_is_160() { assert_eq!(cost_cents(Some(8), Some(false)), 160); }
fn audio_off_8s_is_192() { assert_eq!(cost_cents(Some(8), Some(false)), 192); }
#[test]
fn audio_on_4s_is_160() { assert_eq!(cost_cents(Some(4), Some(true)), 160); }
fn audio_on_4s_is_192() { assert_eq!(cost_cents(Some(4), Some(true)), 192); }
#[test]
fn audio_on_8s_is_320() { assert_eq!(cost_cents(Some(8), Some(true)), 320); }
fn audio_on_8s_is_384() { assert_eq!(cost_cents(Some(8), Some(true)), 384); }
#[test]
fn default_duration_is_8s() {
@@ -15,10 +15,8 @@ impl ArtcraftVeo3FastCostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Mirrors fal_client veo_3_fast: $0.10/sec audio off, $0.15/sec audio on.
// Legacy handler always bills 8s.
let per_second_cents: u64 = if self.generate_audio { 15 } else { 10 };
let cost_in_usd_cents = per_second_cents * 8;
// Flat per-video price: 88¢ audio off, 132¢ audio on.
let cost_in_usd_cents: u64 = if self.generate_audio { 132 } else { 88 };
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -51,22 +49,22 @@ mod tests {
}
#[test]
fn audio_off_is_80() { assert_eq!(cost_cents(Some(8), Some(false)), 80); }
fn audio_off_is_88() { assert_eq!(cost_cents(Some(8), Some(false)), 88); }
#[test]
fn audio_on_is_120() { assert_eq!(cost_cents(Some(8), Some(true)), 120); }
fn audio_on_is_132() { assert_eq!(cost_cents(Some(8), Some(true)), 132); }
#[test]
fn audio_default_is_off() {
assert_eq!(cost_cents(Some(8), None), 80);
assert_eq!(cost_cents(Some(8), None), 88);
}
#[test]
fn duration_does_not_affect_cost() {
// v1 always bills 8s regardless of duration_seconds.
assert_eq!(cost_cents(Some(4), Some(false)), 80);
assert_eq!(cost_cents(Some(6), Some(false)), 80);
assert_eq!(cost_cents(Some(8), Some(false)), 80);
assert_eq!(cost_cents(None, Some(false)), 80);
assert_eq!(cost_cents(Some(4), Some(false)), 88);
assert_eq!(cost_cents(Some(6), Some(false)), 88);
assert_eq!(cost_cents(Some(8), Some(false)), 88);
assert_eq!(cost_cents(None, Some(false)), 88);
}
}
@@ -23,7 +23,7 @@ pub fn build_artcraft_veo_3p1(mut builder: GenerateVideoRequestBuilder) -> Resul
let mut request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Veo3p1,
SupportedResolutions::Full,
SupportedResolutions::FullWith4k,
UltraWideSupport::Unsupported,
)?;
request.generate_audio = generate_audio;
@@ -1,3 +1,5 @@
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::artcraft::veo_3p1::request::ArtcraftVeo3p1RequestState;
@@ -5,6 +7,7 @@ use crate::generate::generate_video::providers::artcraft::veo_3p1::request::Artc
pub struct ArtcraftVeo3p1CostState {
pub duration_seconds: u64,
pub generate_audio: bool,
pub is_4k: bool,
}
impl ArtcraftVeo3p1CostState {
@@ -14,12 +17,19 @@ impl ArtcraftVeo3p1CostState {
duration_seconds: request.request.duration_seconds.map(u64::from).unwrap_or(6),
// v1 legacy Veo 3.1 handler defaults generate_audio to true.
generate_audio: request.request.generate_audio.unwrap_or(true),
is_4k: request.request.resolution == Some(CommonResolutionEnum::FourK),
}
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Mirrors fal_client veo_3p1: $0.20/sec audio off, $0.40/sec audio on.
let per_second_cents: u64 = if self.generate_audio { 40 } else { 20 };
// $0.24/sec audio off, $0.48/sec audio on; 4K renders bill a premium:
// $0.48/sec audio off, $0.72/sec audio on.
let per_second_cents: u64 = match (self.is_4k, self.generate_audio) {
(false, false) => 24,
(false, true) => 48,
(true, false) => 48,
(true, true) => 72,
};
let cost_in_usd_cents = per_second_cents * self.duration_seconds;
VideoGenerationCostEstimate {
@@ -36,12 +46,22 @@ impl ArtcraftVeo3p1CostState {
#[cfg(test)]
mod tests {
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::router_provider::RouterProvider;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
fn cost_cents(duration_seconds: Option<u16>, generate_audio: Option<bool>) -> u64 {
cost_cents_at(duration_seconds, generate_audio, None)
}
fn cost_cents_at(
duration_seconds: Option<u16>,
generate_audio: Option<bool>,
resolution: Option<RouterResolution>,
) -> u64 {
let b = GenerateVideoRequestBuilder {
resolution,
model: RouterVideoModel::Veo3p1,
provider: RouterProvider::Artcraft,
prompt: Some("test".to_string()),
@@ -53,24 +73,42 @@ mod tests {
}
#[test]
fn audio_on_4s_is_160() { assert_eq!(cost_cents(Some(4), Some(true)), 160); }
fn audio_on_4s_is_192() { assert_eq!(cost_cents(Some(4), Some(true)), 192); }
#[test]
fn audio_on_6s_is_240() { assert_eq!(cost_cents(Some(6), Some(true)), 240); }
fn audio_on_6s_is_288() { assert_eq!(cost_cents(Some(6), Some(true)), 288); }
#[test]
fn audio_on_8s_is_320() { assert_eq!(cost_cents(Some(8), Some(true)), 320); }
fn audio_on_8s_is_384() { assert_eq!(cost_cents(Some(8), Some(true)), 384); }
#[test]
fn audio_off_4s_is_80() { assert_eq!(cost_cents(Some(4), Some(false)), 80); }
fn audio_off_4s_is_96() { assert_eq!(cost_cents(Some(4), Some(false)), 96); }
#[test]
fn default_duration_is_6s() {
assert_eq!(cost_cents(None, Some(true)), 240);
assert_eq!(cost_cents(None, Some(true)), 288);
}
#[test]
fn audio_default_is_on() {
assert_eq!(cost_cents(Some(6), None), 240);
assert_eq!(cost_cents(Some(6), None), 288);
}
#[test]
fn four_k_audio_off_4s_is_192() {
assert_eq!(cost_cents_at(Some(4), Some(false), Some(RouterResolution::FourK)), 192);
}
#[test]
fn four_k_audio_on_4s_is_288() {
assert_eq!(cost_cents_at(Some(4), Some(true), Some(RouterResolution::FourK)), 288);
}
#[test]
fn non_four_k_resolutions_share_the_base_rate() {
assert_eq!(
cost_cents_at(Some(8), Some(true), Some(RouterResolution::SevenTwentyP)),
cost_cents_at(Some(8), Some(true), Some(RouterResolution::TenEightyP)),
);
}
}
@@ -20,7 +20,7 @@ pub fn build_artcraft_veo_3p1_fast(mut builder: GenerateVideoRequestBuilder) ->
let mut request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Veo3p1Fast,
SupportedResolutions::Full,
SupportedResolutions::FullWith4k,
UltraWideSupport::Unsupported,
)?;
request.generate_audio = generate_audio;
@@ -1,3 +1,5 @@
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::artcraft::veo_3p1_fast::request::ArtcraftVeo3p1FastRequestState;
@@ -5,6 +7,7 @@ use crate::generate::generate_video::providers::artcraft::veo_3p1_fast::request:
pub struct ArtcraftVeo3p1FastCostState {
pub duration_seconds: u64,
pub generate_audio: bool,
pub is_4k: bool,
}
impl ArtcraftVeo3p1FastCostState {
@@ -14,13 +17,20 @@ impl ArtcraftVeo3p1FastCostState {
duration_seconds: request.request.duration_seconds.map(u64::from).unwrap_or(6),
// v1 legacy Veo 3.1 Fast handler defaults generate_audio to true.
generate_audio: request.request.generate_audio.unwrap_or(true),
is_4k: request.request.resolution == Some(CommonResolutionEnum::FourK),
}
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Mirrors fal_client veo_3p1_fast: $0.10/sec audio off, $0.15/sec audio on.
let per_second_cents: u64 = if self.generate_audio { 15 } else { 10 };
let cost_in_usd_cents = per_second_cents * self.duration_seconds;
// Per-second rates in hundredths of a US cent; 4K renders bill a premium.
let rate_hundredth_cents: u64 = match (self.is_4k, self.generate_audio) {
(false, false) => 1_100,
(false, true) => 1_650,
(true, false) => 3_300,
(true, true) => 3_850,
};
// Round up to the next whole cent.
let cost_in_usd_cents = (rate_hundredth_cents * self.duration_seconds).div_ceil(100);
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -36,12 +46,22 @@ impl ArtcraftVeo3p1FastCostState {
#[cfg(test)]
mod tests {
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::router_provider::RouterProvider;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
fn cost_cents(duration_seconds: Option<u16>, generate_audio: Option<bool>) -> u64 {
cost_cents_at(duration_seconds, generate_audio, None)
}
fn cost_cents_at(
duration_seconds: Option<u16>,
generate_audio: Option<bool>,
resolution: Option<RouterResolution>,
) -> u64 {
let b = GenerateVideoRequestBuilder {
resolution,
model: RouterVideoModel::Veo3p1Fast,
provider: RouterProvider::Artcraft,
prompt: Some("test".to_string()),
@@ -53,24 +73,42 @@ mod tests {
}
#[test]
fn audio_on_4s_is_60() { assert_eq!(cost_cents(Some(4), Some(true)), 60); }
fn audio_on_4s_is_66() { assert_eq!(cost_cents(Some(4), Some(true)), 66); }
#[test]
fn audio_on_6s_is_90() { assert_eq!(cost_cents(Some(6), Some(true)), 90); }
fn audio_on_6s_is_99() { assert_eq!(cost_cents(Some(6), Some(true)), 99); }
#[test]
fn audio_on_8s_is_120() { assert_eq!(cost_cents(Some(8), Some(true)), 120); }
fn audio_on_8s_is_132() { assert_eq!(cost_cents(Some(8), Some(true)), 132); }
#[test]
fn audio_off_4s_is_40() { assert_eq!(cost_cents(Some(4), Some(false)), 40); }
fn audio_off_4s_is_44() { assert_eq!(cost_cents(Some(4), Some(false)), 44); }
#[test]
fn default_duration_is_6s() {
assert_eq!(cost_cents(None, Some(true)), 90);
assert_eq!(cost_cents(None, Some(true)), 99);
}
#[test]
fn audio_default_is_on() {
assert_eq!(cost_cents(Some(6), None), 90);
assert_eq!(cost_cents(Some(6), None), 99);
}
#[test]
fn four_k_audio_off_4s_is_132() {
assert_eq!(cost_cents_at(Some(4), Some(false), Some(RouterResolution::FourK)), 132);
}
#[test]
fn four_k_audio_on_4s_is_154() {
assert_eq!(cost_cents_at(Some(4), Some(true), Some(RouterResolution::FourK)), 154);
}
#[test]
fn non_four_k_resolutions_share_the_base_rate() {
assert_eq!(
cost_cents_at(Some(8), Some(true), Some(RouterResolution::SevenTwentyP)),
cost_cents_at(Some(8), Some(true), Some(RouterResolution::TenEightyP)),
);
}
}
@@ -5,7 +5,7 @@ use crate::generate::generate_video::providers::artcraft::vidu_q3::request::Artc
/// Per-second rates in hundredths of a US cent.
const LOW_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 735;
const HIGH_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 1_617;
const HIGH_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 1_634;
#[derive(Clone, Debug)]
pub struct ArtcraftViduQ3CostState {
@@ -84,13 +84,13 @@ mod tests {
}
#[test]
fn default_resolution_5s_is_81() { assert_eq!(cost_cents(Some(5), None), 81); }
fn default_resolution_5s_is_82() { assert_eq!(cost_cents(Some(5), None), 82); }
#[test]
fn default_duration_is_5s() { assert_eq!(cost_cents(None, None), 81); }
fn default_duration_is_5s() { assert_eq!(cost_cents(None, None), 82); }
#[test]
fn high_res_10s_is_162() { assert_eq!(cost_cents(Some(10), Some(RouterResolution::TenEightyP)), 162); }
fn high_res_10s_is_164() { assert_eq!(cost_cents(Some(10), Some(RouterResolution::TenEightyP)), 164); }
#[test]
fn low_res_5s_is_37() { assert_eq!(cost_cents(Some(5), Some(RouterResolution::FourEightyP)), 37); }
@@ -100,13 +100,13 @@ mod tests {
#[test]
fn seven_twenty_p_is_high_res_tier() {
assert_eq!(cost_cents(Some(8), Some(RouterResolution::SevenTwentyP)), 130);
assert_eq!(cost_cents(Some(8), Some(RouterResolution::SevenTwentyP)), 131);
}
#[test]
fn odd_duration_rounds_up_to_whole_cents() {
// 7s high res: 1617 * 7 = 11319 hundredth-cents → 114 cents.
assert_eq!(cost_cents(Some(7), None), 114);
// 7s high res: 1634 * 7 = 11438 hundredth-cents → 115 cents.
assert_eq!(cost_cents(Some(7), None), 115);
}
#[test]
@@ -4,8 +4,8 @@ use crate::generate::generate_video::video_generation_cost_estimate::VideoGenera
use crate::generate::generate_video::providers::artcraft::vidu_q3_turbo::request::ArtcraftViduQ3TurboRequestState;
/// Per-second rates in hundredths of a US cent.
const LOW_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 368;
const HIGH_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 809;
const LOW_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 401;
const HIGH_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 834;
#[derive(Clone, Debug)]
pub struct ArtcraftViduQ3TurboCostState {
@@ -82,23 +82,23 @@ mod tests {
}
#[test]
fn default_resolution_5s_is_41() { assert_eq!(cost_cents(Some(5), None), 41); }
fn default_resolution_5s_is_42() { assert_eq!(cost_cents(Some(5), None), 42); }
#[test]
fn default_duration_is_5s() { assert_eq!(cost_cents(None, None), 41); }
fn default_duration_is_5s() { assert_eq!(cost_cents(None, None), 42); }
#[test]
fn high_res_10s_is_81() { assert_eq!(cost_cents(Some(10), Some(RouterResolution::TenEightyP)), 81); }
fn high_res_10s_is_84() { assert_eq!(cost_cents(Some(10), Some(RouterResolution::TenEightyP)), 84); }
#[test]
fn low_res_5s_is_19() { assert_eq!(cost_cents(Some(5), Some(RouterResolution::FourEightyP)), 19); }
fn low_res_5s_is_21() { assert_eq!(cost_cents(Some(5), Some(RouterResolution::FourEightyP)), 21); }
#[test]
fn low_res_10s_is_37() { assert_eq!(cost_cents(Some(10), Some(RouterResolution::FourEightyP)), 37); }
fn low_res_10s_is_41() { assert_eq!(cost_cents(Some(10), Some(RouterResolution::FourEightyP)), 41); }
#[test]
fn odd_duration_rounds_up_to_whole_cents() {
// 7s high res: 809 * 7 = 5663 hundredth-cents → 57 cents.
assert_eq!(cost_cents(Some(7), None), 57);
// 7s high res: 834 * 7 = 5838 hundredth-cents → 59 cents.
assert_eq!(cost_cents(Some(7), None), 59);
}
}
@@ -94,6 +94,7 @@ pub fn veo_video_models() -> Vec<OmniGenVideoModelDetails> {
resolution_options: Some(vec![
CommonResolution::SevenTwentyP,
CommonResolution::TenEightyP,
CommonResolution::FourK,
]),
resolution_default: Some(CommonResolution::TenEightyP),
duration_seconds_min: Some(4),
@@ -123,6 +124,7 @@ pub fn veo_video_models() -> Vec<OmniGenVideoModelDetails> {
resolution_options: Some(vec![
CommonResolution::SevenTwentyP,
CommonResolution::TenEightyP,
CommonResolution::FourK,
]),
resolution_default: Some(CommonResolution::TenEightyP),
duration_seconds_min: Some(4),