Compare commits

...
29 changed files with 185 additions and 102 deletions
@@ -9,7 +9,10 @@ use enums::common::generation_provider::GenerationProvider;
pub const ESTIMATE_VIDEO_COST_PATH: &str = "/v1/generate/cost_estimate/video";
/// Request body for the video cost estimate endpoint.
#[derive(Serialize, Deserialize, ToSchema)]
///
/// Every model cares about its own set of parameters and not all
/// parameters may be relevant for every model.
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct EstimateVideoCostRequest {
/// The video model to estimate costs for.
pub model: CommonVideoModel,
@@ -34,10 +37,13 @@ pub struct EstimateVideoCostRequest {
/// Number of videos to generate in parallel.
pub video_batch_count: Option<u16>,
/// Whether to generate audio for the video.
pub generate_audio: Option<bool>,
}
/// Describes the type of video generation being requested.
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Serialize, Deserialize, ToSchema, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum GenerationMode {
TextToVideo,
@@ -99,6 +105,7 @@ mod tests {
resolution: None,
duration_seconds: None,
video_batch_count: None,
generate_audio: None,
};
let serialized = serde_json::to_string(&request).unwrap();
assert!(serialized.contains("\"generation_mode\":{\"type\":\"text_to_video\"}"));
@@ -115,6 +122,7 @@ mod tests {
resolution: None,
duration_seconds: None,
video_batch_count: None,
generate_audio: None,
};
let serialized = serde_json::to_string(&request).unwrap();
assert!(serialized.contains("\"generation_mode\":{\"type\":\"reference_image_to_video\",\"count\":1}"));
@@ -6,7 +6,7 @@ use utoipa::ToSchema;
pub const SEEDANCE_1P5_PRO_MULTI_FUNCTION_VIDEO_GEN_PATH: &str = "/v1/generate/video/multi_function/seedance_1p5_pro";
/// Both text-to-video and image-to-video in one request.
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct Seedance1p5ProMultiFunctionVideoGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
@@ -38,6 +38,9 @@ pub struct Seedance1p5ProMultiFunctionVideoGenRequest {
/// Optional.
pub aspect_ratio: Option<Seedance1p5ProMultiFunctionVideoGenAspectRatio>,
/// Whether to generate audio.
pub generate_audio: Option<bool>,
}
#[derive(Serialize, Deserialize, ToSchema, Copy, Clone, Debug)]
@@ -32,7 +32,7 @@ pub(crate) fn estimate_video_cost_artcraft_kling3p0_pro(
let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str("");
let args = EnqueueKling3p0ProTextToVideoArgs {
prompt: String::new(),
generate_audio: None,
generate_audio: plan.generate_audio,
negative_prompt: None,
duration,
aspect_ratio: None,
@@ -65,6 +65,7 @@ mod tests {
model: CommonVideoModel::Kling3p0Pro,
provider: Provider::Artcraft,
prompt: None,
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
@@ -72,6 +73,7 @@ mod tests {
aspect_ratio: None,
duration_seconds: Some(duration_seconds),
video_batch_count: None,
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
};
@@ -32,7 +32,7 @@ pub(crate) fn estimate_video_cost_artcraft_kling3p0_standard(
let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str("");
let args = EnqueueKling3p0StandardTextToVideoArgs {
prompt: String::new(),
generate_audio: None,
generate_audio: plan.generate_audio,
negative_prompt: None,
duration,
aspect_ratio: None,
@@ -65,6 +65,7 @@ mod tests {
model: CommonVideoModel::Kling3p0Standard,
provider: Provider::Artcraft,
prompt: None,
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
@@ -72,6 +73,7 @@ mod tests {
aspect_ratio: None,
duration_seconds: Some(duration_seconds),
video_batch_count: None,
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
};
@@ -39,6 +39,7 @@ pub(crate) fn estimate_video_cost_artcraft_seedance1p5_pro(
duration,
resolution,
aspect_ratio: None,
generate_audio: plan.generate_audio,
webhook_url: "https://example.com",
api_key: &api_key,
};
@@ -67,6 +68,7 @@ mod tests {
model: CommonVideoModel::Seedance1p5Pro,
provider: Provider::Artcraft,
prompt: None,
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
@@ -74,6 +76,7 @@ mod tests {
aspect_ratio: None,
duration_seconds: Some(duration_seconds),
video_batch_count: None,
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
};
@@ -57,6 +57,7 @@ mod tests {
model: CommonVideoModel::Seedance2p0,
provider: Provider::Artcraft,
prompt: None,
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
@@ -64,6 +65,7 @@ mod tests {
aspect_ratio: None,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(video_batch_count),
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
};
@@ -15,11 +15,11 @@ pub async fn execute_artcraft_kling3p0_pro(
let request = Kling3p0ProMultiFunctionVideoGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
negative_prompt: None,
negative_prompt: plan.negative_prompt.map(|s| s.to_string()),
image_media_token: plan.start_frame.map(|t| t.to_owned()),
end_image_media_token: plan.end_frame.map(|t| t.to_owned()),
duration: plan.duration,
generate_audio: None,
generate_audio: plan.generate_audio,
aspect_ratio: plan.aspect_ratio,
};
@@ -15,11 +15,11 @@ pub async fn execute_artcraft_kling3p0_standard(
let request = Kling3p0StandardMultiFunctionVideoGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
negative_prompt: None,
negative_prompt: plan.negative_prompt.map(|s| s.to_string()),
image_media_token: plan.start_frame.map(|t| t.to_owned()),
end_image_media_token: plan.end_frame.map(|t| t.to_owned()),
duration: plan.duration,
generate_audio: None,
generate_audio: plan.generate_audio,
aspect_ratio: plan.aspect_ratio,
};
@@ -20,6 +20,7 @@ pub async fn execute_artcraft_seedance1p5_pro(
aspect_ratio: plan.aspect_ratio,
duration: plan.duration,
resolution: plan.resolution,
generate_audio: plan.generate_audio,
};
let response = seedance_1p5_pro_multi_function_video_gen(
@@ -22,8 +22,8 @@ pub struct GenerateVideoRequest<'a> {
/// The prompt for the video generation
pub prompt: Option<&'a str>,
// /// Some models support negative prompts
// pub negative_prompt: Option<String>,
/// Some models support negative prompts
pub negative_prompt: Option<&'a str>,
/// Starting keyframe (optional).
pub start_frame: Option<ImageRef<'a>>,
@@ -46,9 +46,11 @@ pub struct GenerateVideoRequest<'a> {
/// How many videos to generate.
pub video_batch_count: Option<u16>,
// /// Whether to turn on/off audio.
// /// Not all models support audio, not all models have a choice.
// pub generate_audio: Option<bool>,
/// Whether to turn on/off audio.
/// Not all models support audio, not all models have a choice.
/// Some models will default this to true, others will default it to false,
/// so it's best to be explicit.
pub generate_audio: Option<bool>,
/// If the request is a mismatch with the (model/provider), how to mitigate it.
pub request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy,
@@ -13,10 +13,12 @@ use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftKling3p0Pro<'a> {
pub prompt: Option<&'a str>,
pub negative_prompt: Option<&'a str>,
pub start_frame: Option<&'a MediaFileToken>,
pub end_frame: Option<&'a MediaFileToken>,
pub aspect_ratio: Option<Kling3p0ProMultiFunctionVideoGenAspectRatio>,
pub duration: Option<Kling3p0ProMultiFunctionVideoGenDuration>,
pub generate_audio: Option<bool>,
pub idempotency_token: String,
}
@@ -33,10 +35,12 @@ pub fn plan_generate_video_artcraft_kling3p0_pro<'a>(
Ok(PlanArtcraftKling3p0Pro {
prompt: request.prompt,
negative_prompt: request.negative_prompt,
start_frame,
end_frame,
aspect_ratio,
duration,
generate_audio: request.generate_audio,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}
@@ -169,6 +173,7 @@ mod tests {
model: CommonVideoModel::Kling3p0Pro,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
@@ -176,6 +181,7 @@ mod tests {
aspect_ratio: None,
duration_seconds: None,
video_batch_count: None,
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
}
@@ -13,10 +13,12 @@ use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftKling3p0Standard<'a> {
pub prompt: Option<&'a str>,
pub negative_prompt: Option<&'a str>,
pub start_frame: Option<&'a MediaFileToken>,
pub end_frame: Option<&'a MediaFileToken>,
pub aspect_ratio: Option<Kling3p0StandardMultiFunctionVideoGenAspectRatio>,
pub duration: Option<Kling3p0StandardMultiFunctionVideoGenDuration>,
pub generate_audio: Option<bool>,
pub idempotency_token: String,
}
@@ -33,10 +35,12 @@ pub fn plan_generate_video_artcraft_kling3p0_standard<'a>(
Ok(PlanArtcraftKling3p0Standard {
prompt: request.prompt,
negative_prompt: request.negative_prompt,
start_frame,
end_frame,
aspect_ratio,
duration,
generate_audio: request.generate_audio,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}
@@ -169,6 +173,7 @@ mod tests {
model: CommonVideoModel::Kling3p0Standard,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
@@ -176,6 +181,7 @@ mod tests {
aspect_ratio: None,
duration_seconds: None,
video_batch_count: None,
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
}
@@ -19,6 +19,7 @@ pub struct PlanArtcraftSeedance1p5Pro<'a> {
pub aspect_ratio: Option<Seedance1p5ProMultiFunctionVideoGenAspectRatio>,
pub duration: Option<Seedance1p5ProMultiFunctionVideoGenDuration>,
pub resolution: Option<Seedance1p5ProMultiFunctionVideoGenResolution>,
pub generate_audio: Option<bool>,
pub idempotency_token: String,
}
@@ -41,6 +42,7 @@ pub fn plan_generate_video_artcraft_seedance1p5_pro<'a>(
aspect_ratio,
duration,
resolution,
generate_audio: request.generate_audio,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}
@@ -184,6 +186,7 @@ mod tests {
model: CommonVideoModel::Seedance1p5Pro,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
@@ -191,6 +194,7 @@ mod tests {
aspect_ratio: None,
duration_seconds: None,
video_batch_count: None,
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
}
@@ -214,6 +214,7 @@ pub fn base_video_request() -> GenerateVideoRequest<'static> {
model: CommonVideoModel::Seedance2p0,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
@@ -221,6 +222,7 @@ pub fn base_video_request() -> GenerateVideoRequest<'static> {
aspect_ratio: None,
duration_seconds: None,
video_batch_count: None,
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
}
@@ -16,6 +16,7 @@ pub struct EnqueueSeedance1p5ProImageToVideoArgs<'a, R: IntoUrl> {
pub resolution: Option<EnqueueSeedance1p5ProImageToVideoResolution>,
pub duration: Option<EnqueueSeedance1p5ProImageToVideoDuration>,
pub aspect_ratio: Option<EnqueueSeedance1p5ProImageToVideoAspectRatio>,
pub generate_audio: Option<bool>,
// Fulfillment
pub webhook_url: R,
@@ -62,10 +63,13 @@ impl <U: IntoUrl> FalRequestCostCalculator for EnqueueSeedance1p5ProImageToVideo
let resolution = self.resolution.unwrap_or(EnqueueSeedance1p5ProImageToVideoResolution::SevenTwentyP);
let duration = self.duration.unwrap_or(EnqueueSeedance1p5ProImageToVideoDuration::FiveSeconds);
let audio = self.generate_audio.unwrap_or(true);
let dollars_per_million_tokens = if audio { 2.4 } else { 1.2 };
if resolution == EnqueueSeedance1p5ProImageToVideoResolution::SevenTwentyP
&& duration == EnqueueSeedance1p5ProImageToVideoDuration::FiveSeconds
{
return 26;
return if audio { 26 } else { 13 };
}
// TODO: Only correct for some aspect ratios for now.
@@ -92,7 +96,7 @@ impl <U: IntoUrl> FalRequestCostCalculator for EnqueueSeedance1p5ProImageToVideo
let tokens = (height as f64) * (width as f64) * FPS * duration_secs;
let tokens = tokens / 1024.0;
let cost = tokens * 2.4 / 1_000_000.0;
let cost = tokens * dollars_per_million_tokens / 1_000_000.0;
let cost = cost * 100.0; // Dollars to cents.
let cost = cost.ceil();
@@ -150,7 +154,7 @@ pub async fn enqueue_seedance_1p5_pro_image_to_video_webhook<R: IntoUrl>(
camera_fixed: None,
seed: None,
enable_safety_checker: Some(false),
generate_audio: Some(true),
generate_audio: Some(args.generate_audio.unwrap_or(true)),
};
let result = seedance_1p5_pro_image_to_video(request)
@@ -183,6 +187,7 @@ mod tests {
duration: Some(EnqueueSeedance1p5ProImageToVideoDuration::FiveSeconds),
resolution: Some(EnqueueSeedance1p5ProImageToVideoResolution::SevenTwentyP),
aspect_ratio: None,
generate_audio: None,
webhook_url: "https://example.com/webhook",
};
@@ -207,6 +212,43 @@ mod tests {
assert_eq!(cost, 146);
}
#[test]
fn test_cost_audio_off() {
let api_key = FalApiKey::from_str("");
let mut args = EnqueueSeedance1p5ProImageToVideoArgs {
prompt: String::new(),
image_url: String::new(),
end_image_url: None,
api_key: &api_key,
duration: Some(EnqueueSeedance1p5ProImageToVideoDuration::FiveSeconds),
resolution: Some(EnqueueSeedance1p5ProImageToVideoResolution::SevenTwentyP),
aspect_ratio: None,
generate_audio: Some(false),
webhook_url: "https://example.com/webhook",
};
// 720p 5s without audio = half of 26
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 13);
// Calculated values — half of audio-on costs (ceil)
args.duration = Some(EnqueueSeedance1p5ProImageToVideoDuration::TenSeconds);
args.resolution = Some(EnqueueSeedance1p5ProImageToVideoResolution::SevenTwentyP);
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 33);
args.duration = Some(EnqueueSeedance1p5ProImageToVideoDuration::FiveSeconds);
args.resolution = Some(EnqueueSeedance1p5ProImageToVideoResolution::TenEightyP);
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 37);
args.duration = Some(EnqueueSeedance1p5ProImageToVideoDuration::TenSeconds);
args.resolution = Some(EnqueueSeedance1p5ProImageToVideoResolution::TenEightyP);
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 73);
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test() -> AnyhowResult<()> {
@@ -220,6 +262,7 @@ mod tests {
aspect_ratio: Some(EnqueueSeedance1p5ProImageToVideoAspectRatio::SixteenByNine),
resolution: Some(EnqueueSeedance1p5ProImageToVideoResolution::SevenTwentyP),
end_image_url: None,
generate_audio: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -245,6 +288,7 @@ mod tests {
aspect_ratio: Some(ar),
resolution: None,
end_image_url: None,
generate_audio: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -270,6 +314,7 @@ mod tests {
aspect_ratio: Some(EnqueueSeedance1p5ProImageToVideoAspectRatio::SixteenByNine),
resolution: None,
end_image_url: None,
generate_audio: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -295,6 +340,7 @@ mod tests {
aspect_ratio: Some(EnqueueSeedance1p5ProImageToVideoAspectRatio::SixteenByNine),
resolution: Some(res),
end_image_url: None,
generate_audio: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -14,6 +14,7 @@ pub struct EnqueueSeedance1p5ProTextToVideoArgs<'a, R: IntoUrl> {
pub resolution: Option<EnqueueSeedance1p5ProTextToVideoResolution>,
pub duration: Option<EnqueueSeedance1p5ProTextToVideoDuration>,
pub aspect_ratio: Option<EnqueueSeedance1p5ProTextToVideoAspectRatio>,
pub generate_audio: Option<bool>,
// Fulfillment
pub webhook_url: R,
@@ -53,17 +54,22 @@ pub enum EnqueueSeedance1p5ProTextToVideoAspectRatio {
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueSeedance1p5ProTextToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Fal:
// "Each 720p 5 second video with audio costs roughly $0.26.
// For other resolutions, 1 million video tokens with audio costs $2.4.
// Without audio, the price is 1.2 per million tokens.
// tokens(video) = (height x width x FPS x duration) / 1024."
let resolution = self.resolution.unwrap_or(EnqueueSeedance1p5ProTextToVideoResolution::SevenTwentyP);
let duration = self.duration.unwrap_or(EnqueueSeedance1p5ProTextToVideoDuration::FiveSeconds);
let audio = self.generate_audio.unwrap_or(true);
let dollars_per_million_tokens = if audio { 2.4 } else { 1.2 };
if resolution == EnqueueSeedance1p5ProTextToVideoResolution::SevenTwentyP
&& duration == EnqueueSeedance1p5ProTextToVideoDuration::FiveSeconds
{
return 26;
return if audio { 26 } else { 13 };
}
// TODO: Only correct for some aspect ratios for now.
@@ -90,7 +96,7 @@ impl <U: IntoUrl> FalRequestCostCalculator for EnqueueSeedance1p5ProTextToVideoA
let tokens = (height as f64) * (width as f64) * FPS * duration_secs;
let tokens = tokens / 1024.0;
let cost = tokens * 2.4 / 1_000_000.0;
let cost = tokens * dollars_per_million_tokens / 1_000_000.0;
let cost = cost * 100.0; // Dollars to cents.
let cost = cost.ceil();
@@ -146,7 +152,7 @@ pub async fn enqueue_seedance_1p5_pro_text_to_video_webhook<R: IntoUrl>(
camera_fixed: None,
seed: None,
enable_safety_checker: Some(false),
generate_audio: Some(true),
generate_audio: Some(args.generate_audio.unwrap_or(true)),
};
let result = seedance_1p5_pro_text_to_video(request)
@@ -176,6 +182,7 @@ mod tests {
duration: Some(EnqueueSeedance1p5ProTextToVideoDuration::FiveSeconds),
resolution: Some(EnqueueSeedance1p5ProTextToVideoResolution::SevenTwentyP),
aspect_ratio: None,
generate_audio: None,
webhook_url: "https://example.com/webhook",
};
@@ -200,6 +207,41 @@ mod tests {
assert_eq!(cost, 146);
}
#[test]
fn test_cost_audio_off() {
let api_key = FalApiKey::from_str("");
let mut args = EnqueueSeedance1p5ProTextToVideoArgs {
prompt: String::new(),
api_key: &api_key,
duration: Some(EnqueueSeedance1p5ProTextToVideoDuration::FiveSeconds),
resolution: Some(EnqueueSeedance1p5ProTextToVideoResolution::SevenTwentyP),
aspect_ratio: None,
generate_audio: Some(false),
webhook_url: "https://example.com/webhook",
};
// 720p 5s without audio = half of 26
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 13);
// Calculated values — half of audio-on costs (ceil)
args.duration = Some(EnqueueSeedance1p5ProTextToVideoDuration::TenSeconds);
args.resolution = Some(EnqueueSeedance1p5ProTextToVideoResolution::SevenTwentyP);
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 33);
args.duration = Some(EnqueueSeedance1p5ProTextToVideoDuration::FiveSeconds);
args.resolution = Some(EnqueueSeedance1p5ProTextToVideoResolution::TenEightyP);
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 37);
args.duration = Some(EnqueueSeedance1p5ProTextToVideoDuration::TenSeconds);
args.resolution = Some(EnqueueSeedance1p5ProTextToVideoResolution::TenEightyP);
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 73);
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test() -> AnyhowResult<()> {
@@ -211,6 +253,7 @@ mod tests {
duration: Some(EnqueueSeedance1p5ProTextToVideoDuration::FiveSeconds),
aspect_ratio: Some(EnqueueSeedance1p5ProTextToVideoAspectRatio::SixteenByNine),
resolution: Some(EnqueueSeedance1p5ProTextToVideoResolution::SevenTwentyP),
generate_audio: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -234,6 +277,7 @@ mod tests {
duration: Some(EnqueueSeedance1p5ProTextToVideoDuration::FourSeconds),
aspect_ratio: Some(ar),
resolution: None,
generate_audio: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -257,6 +301,7 @@ mod tests {
duration: Some(dur),
aspect_ratio: Some(EnqueueSeedance1p5ProTextToVideoAspectRatio::SixteenByNine),
resolution: None,
generate_audio: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -280,6 +325,7 @@ mod tests {
duration: Some(EnqueueSeedance1p5ProTextToVideoDuration::FourSeconds),
aspect_ratio: Some(EnqueueSeedance1p5ProTextToVideoAspectRatio::SixteenByNine),
resolution: Some(res),
generate_audio: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
@@ -7,8 +7,8 @@ use artcraft_api_defs::generate::cost_estimate::estimate_video_cost::{
EstimateVideoCostError, EstimateVideoCostErrorType, EstimateVideoCostRequest,
EstimateVideoCostResponse,
};
use log::info;
use artcraft_client::endpoints::generate::cost_estimate::video::estimate_video_cost::estimate_video_cost;
use log::debug;
use tauri::State;
impl SerializeMarker for EstimateVideoCostResponse {}
@@ -18,7 +18,6 @@ pub async fn estimate_video_cost_command(
request: EstimateVideoCostRequest,
app_env_configs: State<'_, AppEnvConfigs>,
) -> ResponseOrError<EstimateVideoCostResponse, EstimateVideoCostError> {
info!("estimate_video_cost_command called");
let result = estimate_video_cost(
&app_env_configs.storyteller_host,
@@ -1,71 +0,0 @@
use crate::core::commands::enqueue::generate_error::GenerateError;
use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::EnqueueImageToVideoRequest;
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
use crate::core::events::generation_events::common::GenerationModel;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use artcraft_router::api::common_video_model::CommonVideoModel;
use artcraft_router::api::image_ref::ImageRef;
use artcraft_router::api::provider::Provider;
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
use artcraft_router::client::router_client::RouterClient;
use artcraft_router::generate::generate_video::generate_video_request::GenerateVideoRequest;
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_type::TaskType;
use log::{error, info};
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
pub(super) async fn handle_artcraft_seedance_1p5_pro(
request: &EnqueueImageToVideoRequest,
app_env_configs: &AppEnvConfigs,
creds: &StorytellerCredentialSet,
) -> Result<TaskEnqueueSuccess, GenerateError> {
let client = RouterClient::Artcraft(RouterArtcraftClient::new(
app_env_configs.storyteller_host.clone(),
creds.clone(),
));
let start_frame = request.image_media_token.as_ref().map(ImageRef::MediaFileToken);
let end_frame = request.end_frame_image_media_token.as_ref().map(ImageRef::MediaFileToken);
let router_request = GenerateVideoRequest {
model: CommonVideoModel::Seedance1p5Pro,
provider: Provider::Artcraft,
prompt: request.prompt.as_deref(),
start_frame,
end_frame,
reference_images: None,
resolution: None,
aspect_ratio: request.aspect_ratio,
duration_seconds: request.duration_seconds,
video_batch_count: request.video_batch_count,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
idempotency_token: None,
};
let plan = router_request.build()?;
info!("Video Generation Plan: {:?}", plan);
let response = match plan.generate_video(&client).await {
Ok(resp) => {
info!("Successfully enqueued.");
resp
}
Err(err) => {
error!("Failed to enqueue: {:?}", err);
return Err(GenerateError::from(err));
}
};
let job_id = response.get_artcraft_payload()
.map(|p| p.inference_job_token.to_string())
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
Ok(TaskEnqueueSuccess {
task_type: TaskType::VideoGeneration,
model: Some(GenerationModel::Seedance1p5Pro),
provider: GenerationProvider::Artcraft,
provider_job_id: Some(job_id),
})
}
@@ -34,6 +34,7 @@ pub(super) async fn handle_artcraft_seedance_2p0(
model: CommonVideoModel::Seedance2p0,
provider: Provider::Artcraft,
prompt: request.prompt.as_deref(),
negative_prompt: None,
start_frame,
end_frame,
reference_images,
@@ -41,6 +42,7 @@ pub(super) async fn handle_artcraft_seedance_2p0(
aspect_ratio: request.aspect_ratio,
duration_seconds: request.duration_seconds,
video_batch_count: request.video_batch_count,
generate_audio: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
idempotency_token: None,
};
@@ -9,7 +9,6 @@ use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_vi
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_seedance_1_lite::handle_artcraft_seedance_1_lite;
use crate::core::events::generation_events::common::GenerationModel;
use artcraft_router::api::common_video_model::CommonVideoModel;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_seedance_1p5_pro::handle_artcraft_seedance_1p5_pro;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_seedance_2p0::handle_artcraft_seedance_2p0;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_sora2::handle_artcraft_sora2;
use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_sora2_pro::handle_artcraft_sora2_pro;
@@ -46,7 +45,7 @@ pub async fn handle_video_artcraft(
Some(VideoModel::Kling3p0Standard) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Kling3p0Standard, GenerationModel::Kling3p0Standard).await,
Some(VideoModel::Kling3p0Pro) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Kling3p0Pro, GenerationModel::Kling3p0Pro).await,
Some(VideoModel::Seedance10Lite) => handle_artcraft_seedance_1_lite(request, app_env_configs, &creds).await,
Some(VideoModel::Seedance1p5Pro) => handle_artcraft_seedance_1p5_pro(request, app_env_configs, &creds).await,
Some(VideoModel::Seedance1p5Pro) => handle_artcraft_video_via_router(request, app_env_configs, &creds, CommonVideoModel::Seedance1p5Pro, GenerationModel::Seedance1p5Pro).await,
Some(VideoModel::Seedance2p0) => handle_artcraft_seedance_2p0(request, app_env_configs, &creds).await,
Some(VideoModel::Sora2) => handle_artcraft_sora2(request, app_env_configs, &creds).await,
Some(VideoModel::Sora2Pro) => handle_artcraft_sora2_pro(request, app_env_configs, &creds).await,
@@ -34,6 +34,7 @@ pub(super) async fn handle_artcraft_video_via_router(
model,
provider: Provider::Artcraft,
prompt: request.prompt.as_deref(),
negative_prompt: None,
start_frame,
end_frame,
reference_images: None,
@@ -41,6 +42,7 @@ pub(super) async fn handle_artcraft_video_via_router(
aspect_ratio: request.aspect_ratio,
duration_seconds: request.duration_seconds,
video_batch_count: request.video_batch_count,
generate_audio: request.generate_audio,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
idempotency_token: None,
};
@@ -6,7 +6,6 @@ mod handle_artcraft_kling_2p5_turbo_pro;
mod handle_artcraft_kling_2p6_pro;
mod handle_artcraft_video_via_router;
mod handle_artcraft_seedance_1_lite;
mod handle_artcraft_seedance_1p5_pro;
mod handle_artcraft_seedance_2p0;
mod handle_artcraft_sora2;
mod handle_artcraft_sora2_pro;
@@ -42,6 +42,7 @@ pub async fn estimate_video_cost_handler(
model: router_model,
provider: router_provider,
prompt: None,
negative_prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
@@ -49,6 +50,7 @@ pub async fn estimate_video_cost_handler(
aspect_ratio: router_aspect_ratio,
duration_seconds: request.duration_seconds,
video_batch_count: request.video_batch_count,
generate_audio: request.generate_audio,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
idempotency_token: None,
};
@@ -200,6 +200,9 @@ pub async fn kling_3p0_pro_multi_function_video_gen_handler(
let apriori_job_token = InferenceJobToken::generate();
// Most people will want audio
let generate_audio = request.generate_audio.unwrap_or(true);
let fal_result;
if let Some(start_frame_url) = maybe_start_frame_image_url {
@@ -212,7 +215,7 @@ pub async fn kling_3p0_pro_multi_function_video_gen_handler(
prompt: request.prompt.as_deref().unwrap_or("").to_string(),
image_url: start_frame_url,
end_image_url: maybe_end_frame_image_url,
generate_audio: request.generate_audio,
generate_audio: Some(generate_audio),
negative_prompt: request.negative_prompt.clone(),
duration: Some(duration),
aspect_ratio,
@@ -249,7 +252,7 @@ pub async fn kling_3p0_pro_multi_function_video_gen_handler(
let args = EnqueueKling3p0ProTextToVideoArgs {
prompt: request.prompt.as_deref().unwrap_or("").to_string(),
negative_prompt: request.negative_prompt.clone(),
generate_audio: request.generate_audio,
generate_audio: Some(generate_audio),
duration: Some(duration),
aspect_ratio: Some(aspect_ratio),
shot_type: None,
@@ -200,6 +200,9 @@ pub async fn kling_3p0_standard_multi_function_video_gen_handler(
let apriori_job_token = InferenceJobToken::generate();
// Most people will want audio
let generate_audio = request.generate_audio.unwrap_or(true);
let fal_result;
if let Some(start_frame_url) = maybe_start_frame_image_url {
@@ -212,7 +215,7 @@ pub async fn kling_3p0_standard_multi_function_video_gen_handler(
prompt: request.prompt.as_deref().unwrap_or("").to_string(),
image_url: start_frame_url,
end_image_url: maybe_end_frame_image_url,
generate_audio: request.generate_audio,
generate_audio: Some(generate_audio),
negative_prompt: request.negative_prompt.clone(),
duration: Some(duration),
aspect_ratio,
@@ -249,7 +252,7 @@ pub async fn kling_3p0_standard_multi_function_video_gen_handler(
let args = EnqueueKling3p0StandardTextToVideoArgs {
prompt: request.prompt.as_deref().unwrap_or("").to_string(),
negative_prompt: request.negative_prompt.clone(),
generate_audio: request.generate_audio,
generate_audio: Some(generate_audio),
duration: Some(duration),
aspect_ratio: Some(aspect_ratio),
shot_type: None,
@@ -120,6 +120,8 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<Seedance1p5ProMultiFunctionVideoGenResponse>, CommonWebError> {
info!("Request: {:?}", request);
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
let mut mysql_connection = server_state.mysql_pool
@@ -204,6 +206,9 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
let apriori_job_token = InferenceJobToken::generate();
// Most people will want audio
let generate_audio = request.generate_audio.unwrap_or(true);
let fal_result;
if let Some(image_url) = maybe_image_url {
@@ -220,6 +225,7 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
duration: Some(duration),
aspect_ratio: Some(aspect_ratio),
resolution: Some(resolution),
generate_audio: Some(generate_audio),
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
@@ -254,6 +260,7 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
duration: Some(duration),
aspect_ratio: Some(aspect_ratio),
resolution: Some(resolution),
generate_audio: Some(generate_audio),
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
@@ -29,6 +29,7 @@ export function useVideoCostEstimate(
const inputMode = usePromptVideoStore((s) => s.inputMode);
const referenceImages = usePromptVideoStore((s) => s.referenceImages);
const endFrameImage = usePromptVideoStore((s) => s.endFrameImage);
const generateWithSound = usePromptVideoStore((s) => s.generateWithSound);
useEffect(() => {
if (activePage !== ModelPage.ImageToVideo || !selectedModel) {
@@ -65,6 +66,7 @@ export function useVideoCostEstimate(
generation_mode: generationMode,
aspect_ratio: commonAspectRatio ?? undefined,
duration_seconds: duration ?? undefined,
generate_audio: generateWithSound,
})
.then((result) => {
if (isEstimateVideoCostSuccess(result)) {
@@ -89,6 +91,7 @@ export function useVideoCostEstimate(
inputMode,
referenceImages.length,
!!endFrameImage,
generateWithSound,
]);
return { isLoading };
@@ -144,7 +144,7 @@ export const VIDEO_MODELS: VideoModel[] = [
durationOptions: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
defaultDuration: 5,
progressBarTime: 100000,
generateWithSound: false,
generateWithSound: true,
}),
new VideoModel({
id: "kling_3p0_pro",
@@ -179,7 +179,7 @@ export const VIDEO_MODELS: VideoModel[] = [
durationOptions: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
defaultDuration: 5,
progressBarTime: 100000,
generateWithSound: false,
generateWithSound: true,
}),
new VideoModel({
id: "seedance_1_0_lite",
@@ -244,6 +244,7 @@ export const VIDEO_MODELS: VideoModel[] = [
resolutionOptions: ["480p", "720p", "1080p"],
defaultResolution: "720p",
supportsSystemPrompt: false,
generateWithSound: true,
progressBarTime: 100000,
}),
new VideoModel({
@@ -16,6 +16,7 @@ export interface EstimateVideoCostRequest {
resolution?: CommonVideoResolution;
duration_seconds?: number;
video_batch_count?: number;
generate_audio?: boolean;
}
export interface EstimateVideoCostPayload {