From 3023ade0cca192ee5e773ee36a2b5a073d1966aa Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 8 Mar 2026 13:13:07 -0400 Subject: [PATCH] Kling 3.0 --- .../kling_3p0_pro_multi_function_video_gen.rs | 66 +++ ...g_3p0_standard_multi_function_video_gen.rs | 66 +++ .../src/generate/video/multi_function/mod.rs | 2 + .../kling_3p0_pro_multi_function_video_gen.rs | 18 + ...g_3p0_standard_multi_function_video_gen.rs | 18 + .../generate/video/multi_function/mod.rs | 2 + .../src/api/common_video_model.rs | 6 + ...timate_video_cost_artcraft_kling3p0_pro.rs | 95 +++++ ...e_video_cost_artcraft_kling3p0_standard.rs | 95 +++++ .../generate_video/cost/artcraft/mod.rs | 2 + .../generate_video_artcraft_kling3p0_pro.rs | 39 ++ ...nerate_video_artcraft_kling3p0_standard.rs | 39 ++ .../generate_video/execute/artcraft/mod.rs | 2 + .../generate_video/generate_video_request.rs | 8 + .../generate_video/plan/artcraft/mod.rs | 2 + ...an_generate_video_artcraft_kling3p0_pro.rs | 266 ++++++++++++ ...nerate_video_artcraft_kling3p0_standard.rs | 266 ++++++++++++ .../generate_video/video_generation_plan.rs | 22 + ...eue_kling_v3_pro_image_to_video_webhook.rs | 105 +++++ ...ling_v3_standard_image_to_video_webhook.rs | 104 +++++ .../src/requests/webhook/video/image/mod.rs | 2 + ...ueue_kling_v3_pro_text_to_video_webhook.rs | 201 ++++++++++ ...kling_v3_standard_text_to_video_webhook.rs | 218 ++++++++++ .../src/requests/webhook/video/text/mod.rs | 2 + .../artcraft/handle_artcraft_kling_3p0_pro.rs | 71 ++++ .../handle_artcraft_kling_3p0_standard.rs | 71 ++++ .../artcraft/handle_artcraft_video.rs | 4 + .../enqueue/image_to_video/artcraft/mod.rs | 2 + .../enqueue_image_to_video_command.rs | 6 + .../commands/enqueue/task_enqueue_success.rs | 2 + .../core/events/generation_events/common.rs | 4 + .../common/generation/common_video_model.rs | 6 + .../public/enums/src/common/model_type.rs | 20 +- .../enums/src/tauri/tasks/task_model_type.rs | 20 +- .../web/storyteller_web/src/docs/api_doc.rs | 12 + .../video/estimate_video_cost_handler.rs | 2 + ...p0_pro_multi_function_video_gen_handler.rs | 377 ++++++++++++++++++ ...andard_multi_function_video_gen_handler.rs | 377 ++++++++++++++++++ .../generate/video/multi_function/mod.rs | 2 + .../application_routes/generate_routes.rs | 10 + .../src/endpoints/fal_ai/kling_video/mod.rs | 14 + .../v3/kling_v3_pro_image_to_video.rs | 53 +++ .../v3/kling_v3_pro_text_to_video.rs | 46 +++ .../v3/kling_v3_standard_image_to_video.rs | 53 +++ .../v3/kling_v3_standard_text_to_video.rs | 46 +++ .../endpoints/fal_ai/kling_video/v3/mod.rs | 4 + .../apps/artcraft-website/tsconfig.app.json | 15 +- frontend/apps/artcraft-website/tsconfig.json | 15 +- .../lib/common/generation/CommonVideoModel.ts | 2 + .../convert/videoModelToCommonVideoModel.ts | 6 + .../src/lib/cost-breakdown-modal.tsx | 3 + .../model-list/src/lib/lists/VideoModels.ts | 68 ++++ 52 files changed, 2943 insertions(+), 14 deletions(-) create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/kling_3p0_pro_multi_function_video_gen.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/kling_3p0_standard_multi_function_video_gen.rs create mode 100644 crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/multi_function/kling_3p0_pro_multi_function_video_gen.rs create mode 100644 crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/multi_function/kling_3p0_standard_multi_function_video_gen.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_kling3p0_pro.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_kling3p0_standard.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_kling3p0_pro.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_kling3p0_standard.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/plan_generate_video_artcraft_kling3p0_pro.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/plan_generate_video_artcraft_kling3p0_standard.rs create mode 100644 crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v3_pro_image_to_video_webhook.rs create mode 100644 crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v3_standard_image_to_video_webhook.rs create mode 100644 crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v3_pro_text_to_video_webhook.rs create mode 100644 crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v3_standard_text_to_video_webhook.rs create mode 100644 crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/handle_artcraft_kling_3p0_pro.rs create mode 100644 crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/handle_artcraft_kling_3p0_standard.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/kling_3p0_pro_multi_function_video_gen_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/kling_3p0_standard_multi_function_video_gen_handler.rs create mode 100644 crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_pro_image_to_video.rs create mode 100644 crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_pro_text_to_video.rs create mode 100644 crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_standard_image_to_video.rs create mode 100644 crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_standard_text_to_video.rs create mode 100644 crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/mod.rs diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/kling_3p0_pro_multi_function_video_gen.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/kling_3p0_pro_multi_function_video_gen.rs new file mode 100644 index 0000000000..7707c367e7 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/kling_3p0_pro_multi_function_video_gen.rs @@ -0,0 +1,66 @@ +use serde::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const KLING_3P0_PRO_MULTI_FUNCTION_VIDEO_PATH: &str = "/v1/generate/video/multi_function/kling_3p0_pro"; + +/// Both text-to-video and image-to-video in one request. +#[derive(Serialize, Deserialize, ToSchema)] +pub struct Kling3p0ProMultiFunctionVideoGenRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Text prompt. + pub prompt: Option, + + /// Optional negative prompt. + pub negative_prompt: Option, + + /// If present, we're doing image-to-video. If absent, text-to-video. + pub image_media_token: Option, + + /// Optional end frame image. + pub end_image_media_token: Option, + + /// Duration of the video (3-15 seconds). + pub duration: Option, + + /// Whether to generate audio. + pub generate_audio: Option, + + /// Aspect ratio (only for text-to-video). + pub aspect_ratio: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum Kling3p0ProMultiFunctionVideoGenDuration { + ThreeSeconds, + FourSeconds, + FiveSeconds, + SixSeconds, + SevenSeconds, + EightSeconds, + NineSeconds, + TenSeconds, + ElevenSeconds, + TwelveSeconds, + ThirteenSeconds, + FourteenSeconds, + FifteenSeconds, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum Kling3p0ProMultiFunctionVideoGenAspectRatio { + Square, + SixteenByNine, + NineBySixteen, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct Kling3p0ProMultiFunctionVideoGenResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/kling_3p0_standard_multi_function_video_gen.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/kling_3p0_standard_multi_function_video_gen.rs new file mode 100644 index 0000000000..e5b0484fff --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/kling_3p0_standard_multi_function_video_gen.rs @@ -0,0 +1,66 @@ +use serde::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const KLING_3P0_STANDARD_MULTI_FUNCTION_VIDEO_PATH: &str = "/v1/generate/video/multi_function/kling_3p0_standard"; + +/// Both text-to-video and image-to-video in one request. +#[derive(Serialize, Deserialize, ToSchema)] +pub struct Kling3p0StandardMultiFunctionVideoGenRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Text prompt. + pub prompt: Option, + + /// Optional negative prompt. + pub negative_prompt: Option, + + /// If present, we're doing image-to-video. If absent, text-to-video. + pub image_media_token: Option, + + /// Optional end frame image. + pub end_image_media_token: Option, + + /// Duration of the video (3-15 seconds). + pub duration: Option, + + /// Whether to generate audio. + pub generate_audio: Option, + + /// Aspect ratio (only for text-to-video). + pub aspect_ratio: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum Kling3p0StandardMultiFunctionVideoGenDuration { + ThreeSeconds, + FourSeconds, + FiveSeconds, + SixSeconds, + SevenSeconds, + EightSeconds, + NineSeconds, + TenSeconds, + ElevenSeconds, + TwelveSeconds, + ThirteenSeconds, + FourteenSeconds, + FifteenSeconds, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum Kling3p0StandardMultiFunctionVideoGenAspectRatio { + Square, + SixteenByNine, + NineBySixteen, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct Kling3p0StandardMultiFunctionVideoGenResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/mod.rs index 0d1e71a6e3..e29945ab86 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/mod.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/mod.rs @@ -1,5 +1,7 @@ pub mod kling_2_5_turbo_multi_function_video_gen; pub mod kling_2_6_multi_function_video_gen; +pub mod kling_3p0_pro_multi_function_video_gen; +pub mod kling_3p0_standard_multi_function_video_gen; pub mod seedance_1p5_pro_multi_function_video_gen; pub mod seedance_2p0_multi_function_video_gen; pub mod sora_2_multi_function_video_gen; diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/multi_function/kling_3p0_pro_multi_function_video_gen.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/multi_function/kling_3p0_pro_multi_function_video_gen.rs new file mode 100644 index 0000000000..a65486b846 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/multi_function/kling_3p0_pro_multi_function_video_gen.rs @@ -0,0 +1,18 @@ +use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; +use crate::error::storyteller_error::StorytellerError; +use crate::utils::api_host::ApiHost; +use crate::utils::basic_json_post_request::basic_json_post_request; +use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::{Kling3p0ProMultiFunctionVideoGenRequest, Kling3p0ProMultiFunctionVideoGenResponse, KLING_3P0_PRO_MULTI_FUNCTION_VIDEO_PATH}; + +pub async fn kling_3p0_pro_multi_function_video_gen( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + request: Kling3p0ProMultiFunctionVideoGenRequest, +) -> Result { + Ok(basic_json_post_request( + api_host, + KLING_3P0_PRO_MULTI_FUNCTION_VIDEO_PATH, + maybe_creds, + request, + ).await?) +} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/multi_function/kling_3p0_standard_multi_function_video_gen.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/multi_function/kling_3p0_standard_multi_function_video_gen.rs new file mode 100644 index 0000000000..c84bb70ee7 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/multi_function/kling_3p0_standard_multi_function_video_gen.rs @@ -0,0 +1,18 @@ +use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; +use crate::error::storyteller_error::StorytellerError; +use crate::utils::api_host::ApiHost; +use crate::utils::basic_json_post_request::basic_json_post_request; +use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::{Kling3p0StandardMultiFunctionVideoGenRequest, Kling3p0StandardMultiFunctionVideoGenResponse, KLING_3P0_STANDARD_MULTI_FUNCTION_VIDEO_PATH}; + +pub async fn kling_3p0_standard_multi_function_video_gen( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + request: Kling3p0StandardMultiFunctionVideoGenRequest, +) -> Result { + Ok(basic_json_post_request( + api_host, + KLING_3P0_STANDARD_MULTI_FUNCTION_VIDEO_PATH, + maybe_creds, + request, + ).await?) +} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/multi_function/mod.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/multi_function/mod.rs index 9839331bc1..f829677416 100644 --- a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/multi_function/mod.rs +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/multi_function/mod.rs @@ -2,6 +2,8 @@ pub mod kling_2p5_turbo_pro_multi_function_video_gen; pub mod seedance_1p5_pro_multi_function_video_gen; pub mod seedance_2p0_multi_function_video_gen; pub mod kling_2p6_pro_multi_function_video_gen; +pub mod kling_3p0_pro_multi_function_video_gen; +pub mod kling_3p0_standard_multi_function_video_gen; pub mod sora_2_multi_function_video_gen; pub mod sora_2_pro_multi_function_video_gen; pub mod veo_3p1_fast_multi_function_video_gen; diff --git a/crates/api_clients/artcraft/artcraft_router/src/api/common_video_model.rs b/crates/api_clients/artcraft/artcraft_router/src/api/common_video_model.rs index 833d0bedd9..bf1989086b 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/api/common_video_model.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/api/common_video_model.rs @@ -23,6 +23,12 @@ pub enum CommonVideoModel { #[serde(rename = "kling_2p6_pro")] Kling2p6Pro, + #[serde(rename = "kling_3p0_standard")] + Kling3p0Standard, + + #[serde(rename = "kling_3p0_pro")] + Kling3p0Pro, + #[serde(rename = "seedance_1p0_lite")] Seedance10Lite, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_kling3p0_pro.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_kling3p0_pro.rs new file mode 100644 index 0000000000..4d9efd946a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_kling3p0_pro.rs @@ -0,0 +1,95 @@ +use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::Kling3p0ProMultiFunctionVideoGenDuration; +use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator; +use fal_client::requests::webhook::video::text::enqueue_kling_v3_pro_text_to_video_webhook::{ + EnqueueKlingV3ProTextToVideoArgs, + EnqueueKlingV3ProTextToVideoDuration, +}; + +use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::PlanArtcraftKling3p0Pro; +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; + +pub(crate) fn estimate_video_cost_artcraft_kling3p0_pro( + plan: &PlanArtcraftKling3p0Pro<'_>, +) -> VideoGenerationCostEstimate { + let duration = plan.duration + .map(|d| match d { + Kling3p0ProMultiFunctionVideoGenDuration::ThreeSeconds => EnqueueKlingV3ProTextToVideoDuration::ThreeSeconds, + Kling3p0ProMultiFunctionVideoGenDuration::FourSeconds => EnqueueKlingV3ProTextToVideoDuration::FourSeconds, + Kling3p0ProMultiFunctionVideoGenDuration::FiveSeconds => EnqueueKlingV3ProTextToVideoDuration::FiveSeconds, + Kling3p0ProMultiFunctionVideoGenDuration::SixSeconds => EnqueueKlingV3ProTextToVideoDuration::SixSeconds, + Kling3p0ProMultiFunctionVideoGenDuration::SevenSeconds => EnqueueKlingV3ProTextToVideoDuration::SevenSeconds, + Kling3p0ProMultiFunctionVideoGenDuration::EightSeconds => EnqueueKlingV3ProTextToVideoDuration::EightSeconds, + Kling3p0ProMultiFunctionVideoGenDuration::NineSeconds => EnqueueKlingV3ProTextToVideoDuration::NineSeconds, + Kling3p0ProMultiFunctionVideoGenDuration::TenSeconds => EnqueueKlingV3ProTextToVideoDuration::TenSeconds, + Kling3p0ProMultiFunctionVideoGenDuration::ElevenSeconds => EnqueueKlingV3ProTextToVideoDuration::ElevenSeconds, + Kling3p0ProMultiFunctionVideoGenDuration::TwelveSeconds => EnqueueKlingV3ProTextToVideoDuration::TwelveSeconds, + Kling3p0ProMultiFunctionVideoGenDuration::ThirteenSeconds => EnqueueKlingV3ProTextToVideoDuration::ThirteenSeconds, + Kling3p0ProMultiFunctionVideoGenDuration::FourteenSeconds => EnqueueKlingV3ProTextToVideoDuration::FourteenSeconds, + Kling3p0ProMultiFunctionVideoGenDuration::FifteenSeconds => EnqueueKlingV3ProTextToVideoDuration::FifteenSeconds, + }); + + // Use text-to-video cost calculator (same pricing for both modes) + let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str(""); + let args = EnqueueKlingV3ProTextToVideoArgs { + prompt: String::new(), + generate_audio: None, + negative_prompt: None, + duration, + aspect_ratio: None, + webhook_url: "https://example.com", + api_key: &api_key, + }; + + let cost_in_usd_cents = args.calculate_cost_in_cents(); + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_video_model::CommonVideoModel; + use crate::api::provider::Provider; + use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; + use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; + + fn estimate_usd_cents(duration_seconds: u16) -> u64 { + let request = GenerateVideoRequest { + model: CommonVideoModel::Kling3p0Pro, + provider: Provider::Artcraft, + prompt: None, + start_frame: None, + end_frame: None, + reference_images: None, + resolution: None, + aspect_ratio: None, + duration_seconds: Some(duration_seconds), + video_batch_count: None, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + idempotency_token: None, + }; + request.build() + .expect("build should succeed") + .estimate_costs() + .cost_in_usd_cents + .expect("cost_in_usd_cents should be present") + } + + #[test] + fn test_estimate_cost_usd_cents() { + // Kling 3.0 Pro: $0.336/sec (audio on, the default when None) + // Formula: (336 * duration_secs + 9) / 10 → ceiling division + // 5s: (336 * 5 + 9) / 10 = 1689 / 10 = 168 + assert_eq!(estimate_usd_cents(5), 168); + // 10s: (336 * 10 + 9) / 10 = 3369 / 10 = 336 + assert_eq!(estimate_usd_cents(10), 336); + // 15s: (336 * 15 + 9) / 10 = 5049 / 10 = 504 + assert_eq!(estimate_usd_cents(15), 504); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_kling3p0_standard.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_kling3p0_standard.rs new file mode 100644 index 0000000000..f965fc748f --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_kling3p0_standard.rs @@ -0,0 +1,95 @@ +use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::Kling3p0StandardMultiFunctionVideoGenDuration; +use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator; +use fal_client::requests::webhook::video::text::enqueue_kling_v3_standard_text_to_video_webhook::{ + EnqueueKlingV3StandardTextToVideoArgs, + EnqueueKlingV3StandardTextToVideoDuration, +}; + +use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::PlanArtcraftKling3p0Standard; +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; + +pub(crate) fn estimate_video_cost_artcraft_kling3p0_standard( + plan: &PlanArtcraftKling3p0Standard<'_>, +) -> VideoGenerationCostEstimate { + let duration = plan.duration + .map(|d| match d { + Kling3p0StandardMultiFunctionVideoGenDuration::ThreeSeconds => EnqueueKlingV3StandardTextToVideoDuration::ThreeSeconds, + Kling3p0StandardMultiFunctionVideoGenDuration::FourSeconds => EnqueueKlingV3StandardTextToVideoDuration::FourSeconds, + Kling3p0StandardMultiFunctionVideoGenDuration::FiveSeconds => EnqueueKlingV3StandardTextToVideoDuration::FiveSeconds, + Kling3p0StandardMultiFunctionVideoGenDuration::SixSeconds => EnqueueKlingV3StandardTextToVideoDuration::SixSeconds, + Kling3p0StandardMultiFunctionVideoGenDuration::SevenSeconds => EnqueueKlingV3StandardTextToVideoDuration::SevenSeconds, + Kling3p0StandardMultiFunctionVideoGenDuration::EightSeconds => EnqueueKlingV3StandardTextToVideoDuration::EightSeconds, + Kling3p0StandardMultiFunctionVideoGenDuration::NineSeconds => EnqueueKlingV3StandardTextToVideoDuration::NineSeconds, + Kling3p0StandardMultiFunctionVideoGenDuration::TenSeconds => EnqueueKlingV3StandardTextToVideoDuration::TenSeconds, + Kling3p0StandardMultiFunctionVideoGenDuration::ElevenSeconds => EnqueueKlingV3StandardTextToVideoDuration::ElevenSeconds, + Kling3p0StandardMultiFunctionVideoGenDuration::TwelveSeconds => EnqueueKlingV3StandardTextToVideoDuration::TwelveSeconds, + Kling3p0StandardMultiFunctionVideoGenDuration::ThirteenSeconds => EnqueueKlingV3StandardTextToVideoDuration::ThirteenSeconds, + Kling3p0StandardMultiFunctionVideoGenDuration::FourteenSeconds => EnqueueKlingV3StandardTextToVideoDuration::FourteenSeconds, + Kling3p0StandardMultiFunctionVideoGenDuration::FifteenSeconds => EnqueueKlingV3StandardTextToVideoDuration::FifteenSeconds, + }); + + // Use text-to-video cost calculator (same pricing for both modes) + let api_key = fal_client::creds::fal_api_key::FalApiKey::from_str(""); + let args = EnqueueKlingV3StandardTextToVideoArgs { + prompt: String::new(), + generate_audio: None, + negative_prompt: None, + duration, + aspect_ratio: None, + webhook_url: "https://example.com", + api_key: &api_key, + }; + + let cost_in_usd_cents = args.calculate_cost_in_cents(); + + VideoGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_video_model::CommonVideoModel; + use crate::api::provider::Provider; + use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; + use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; + + fn estimate_usd_cents(duration_seconds: u16) -> u64 { + let request = GenerateVideoRequest { + model: CommonVideoModel::Kling3p0Standard, + provider: Provider::Artcraft, + prompt: None, + start_frame: None, + end_frame: None, + reference_images: None, + resolution: None, + aspect_ratio: None, + duration_seconds: Some(duration_seconds), + video_batch_count: None, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + idempotency_token: None, + }; + request.build() + .expect("build should succeed") + .estimate_costs() + .cost_in_usd_cents + .expect("cost_in_usd_cents should be present") + } + + #[test] + fn test_estimate_cost_usd_cents() { + // Kling 3.0 Standard: $0.252/sec (audio on, the default when None) + // Formula: (252 * duration_secs + 9) / 10 → ceiling division + // 5s: (252 * 5 + 9) / 10 = 1269 / 10 = 126 + assert_eq!(estimate_usd_cents(5), 126); + // 10s: (252 * 10 + 9) / 10 = 2529 / 10 = 252 + assert_eq!(estimate_usd_cents(10), 252); + // 15s: (252 * 15 + 9) / 10 = 3789 / 10 = 378 + assert_eq!(estimate_usd_cents(15), 378); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/mod.rs index e3f3ff0b79..1e50fb4d62 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/mod.rs @@ -1,2 +1,4 @@ +pub mod estimate_video_cost_artcraft_kling3p0_pro; +pub mod estimate_video_cost_artcraft_kling3p0_standard; pub mod estimate_video_cost_artcraft_seedance1p5_pro; pub mod estimate_video_cost_artcraft_seedance2p0; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_kling3p0_pro.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_kling3p0_pro.rs new file mode 100644 index 0000000000..ea84236730 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_kling3p0_pro.rs @@ -0,0 +1,39 @@ +use crate::client::router_artcraft_client::RouterArtcraftClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::PlanArtcraftKling3p0Pro; +use crate::generate::generate_video::generate_video_response::{ + ArtcraftVideoResponsePayload, GenerateVideoResponse, +}; +use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::Kling3p0ProMultiFunctionVideoGenRequest; +use artcraft_client::endpoints::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::kling_3p0_pro_multi_function_video_gen; + +pub async fn execute_artcraft_kling3p0_pro( + plan: &PlanArtcraftKling3p0Pro<'_>, + artcraft_client: &RouterArtcraftClient, +) -> Result { + let request = Kling3p0ProMultiFunctionVideoGenRequest { + uuid_idempotency_token: plan.idempotency_token.clone(), + prompt: plan.prompt.map(|p| p.to_string()), + negative_prompt: None, + 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, + aspect_ratio: plan.aspect_ratio, + }; + + let response = kling_3p0_pro_multi_function_video_gen( + &artcraft_client.api_host, + Some(&artcraft_client.credentials), + request, + ) + .await + .map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?; + + let all_tokens = vec![response.inference_job_token.clone()]; + Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload { + inference_job_token: response.inference_job_token, + all_inference_job_tokens: all_tokens, + })) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_kling3p0_standard.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_kling3p0_standard.rs new file mode 100644 index 0000000000..0520fc4ba6 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_kling3p0_standard.rs @@ -0,0 +1,39 @@ +use crate::client::router_artcraft_client::RouterArtcraftClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::PlanArtcraftKling3p0Standard; +use crate::generate::generate_video::generate_video_response::{ + ArtcraftVideoResponsePayload, GenerateVideoResponse, +}; +use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::Kling3p0StandardMultiFunctionVideoGenRequest; +use artcraft_client::endpoints::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::kling_3p0_standard_multi_function_video_gen; + +pub async fn execute_artcraft_kling3p0_standard( + plan: &PlanArtcraftKling3p0Standard<'_>, + artcraft_client: &RouterArtcraftClient, +) -> Result { + let request = Kling3p0StandardMultiFunctionVideoGenRequest { + uuid_idempotency_token: plan.idempotency_token.clone(), + prompt: plan.prompt.map(|p| p.to_string()), + negative_prompt: None, + 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, + aspect_ratio: plan.aspect_ratio, + }; + + let response = kling_3p0_standard_multi_function_video_gen( + &artcraft_client.api_host, + Some(&artcraft_client.credentials), + request, + ) + .await + .map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?; + + let all_tokens = vec![response.inference_job_token.clone()]; + Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload { + inference_job_token: response.inference_job_token, + all_inference_job_tokens: all_tokens, + })) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/mod.rs index 497c815199..8922bee6c7 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/mod.rs @@ -1,2 +1,4 @@ +pub mod generate_video_artcraft_kling3p0_pro; +pub mod generate_video_artcraft_kling3p0_standard; pub mod generate_video_artcraft_seedance1p5_pro; pub mod generate_video_artcraft_seedance2p0; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request.rs index a17a57dea7..c7890eee5f 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request.rs @@ -6,6 +6,8 @@ use crate::api::image_ref::ImageRef; use crate::api::provider::Provider; use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::plan_generate_video_artcraft_kling3p0_pro; +use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::plan_generate_video_artcraft_kling3p0_standard; use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::plan_generate_video_artcraft_seedance1p5_pro; use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::plan_generate_video_artcraft_seedance2p0; use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan; @@ -61,6 +63,12 @@ impl<'a> GenerateVideoRequest<'a> { pub fn build(&self) -> Result, ArtcraftRouterError> { match self.provider { Provider::Artcraft => match self.model { + CommonVideoModel::Kling3p0Pro => { + plan_generate_video_artcraft_kling3p0_pro(self).map(VideoGenerationPlan::ArtcraftKling3p0Pro) + } + CommonVideoModel::Kling3p0Standard => { + plan_generate_video_artcraft_kling3p0_standard(self).map(VideoGenerationPlan::ArtcraftKling3p0Standard) + } CommonVideoModel::Seedance1p5Pro => { plan_generate_video_artcraft_seedance1p5_pro(self).map(VideoGenerationPlan::ArtcraftSeedance1p5Pro) } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/mod.rs index e2a6604f1f..98ba83520f 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/mod.rs @@ -1,2 +1,4 @@ +pub mod plan_generate_video_artcraft_kling3p0_pro; +pub mod plan_generate_video_artcraft_kling3p0_standard; pub mod plan_generate_video_artcraft_seedance1p5_pro; pub mod plan_generate_video_artcraft_seedance2p0; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/plan_generate_video_artcraft_kling3p0_pro.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/plan_generate_video_artcraft_kling3p0_pro.rs new file mode 100644 index 0000000000..dc74c8c143 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/plan_generate_video_artcraft_kling3p0_pro.rs @@ -0,0 +1,266 @@ +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::image_ref::ImageRef; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; +use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::{ + Kling3p0ProMultiFunctionVideoGenAspectRatio, + Kling3p0ProMultiFunctionVideoGenDuration, +}; +use tokens::tokens::media_files::MediaFileToken; + +#[derive(Debug, Clone)] +pub struct PlanArtcraftKling3p0Pro<'a> { + pub prompt: Option<&'a str>, + pub start_frame: Option<&'a MediaFileToken>, + pub end_frame: Option<&'a MediaFileToken>, + pub aspect_ratio: Option, + pub duration: Option, + pub idempotency_token: String, +} + +pub fn plan_generate_video_artcraft_kling3p0_pro<'a>( + request: &'a GenerateVideoRequest<'a>, +) -> Result, ArtcraftRouterError> { + let strategy = request.request_mismatch_mitigation_strategy; + + let start_frame = resolve_image_ref(request.start_frame)?; + let end_frame = resolve_image_ref(request.end_frame)?; + + let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?; + let duration = plan_duration(request.duration_seconds, strategy)?; + + Ok(PlanArtcraftKling3p0Pro { + prompt: request.prompt, + start_frame, + end_frame, + aspect_ratio, + duration, + idempotency_token: request.get_or_generate_idempotency_token(), + }) +} + +fn resolve_image_ref<'a>( + image_ref: Option>, +) -> Result, ArtcraftRouterError> { + match image_ref { + None => Ok(None), + Some(ImageRef::MediaFileToken(t)) => Ok(Some(t)), + Some(ImageRef::Url(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens)) + } + } +} + +// Kling 3.0 Pro supports: Square (1:1), SixteenByNine (16:9), NineBySixteen (9:16) +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match aspect_ratio { + None => Ok(None), + + // Direct mappings + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => { + Ok(Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::SixteenByNine)) + } + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => { + Ok(Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::NineBySixteen)) + } + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => { + Ok(Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::Square)) + } + + // Auto defaults to 16:9 + Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => { + Ok(Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::SixteenByNine)) + } + + // Mismatches + Some(unsupported) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade + | RequestMismatchMitigationStrategy::PayLessDowngrade => { + Ok(Some(nearest_aspect_ratio(unsupported))) + } + }, + } +} + +fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> Kling3p0ProMultiFunctionVideoGenAspectRatio { + match aspect_ratio { + // Wide-ish → 16:9 + CommonAspectRatio::WideFourByThree + | CommonAspectRatio::WideFiveByFour + | CommonAspectRatio::WideThreeByTwo + | CommonAspectRatio::WideTwentyOneByNine => Kling3p0ProMultiFunctionVideoGenAspectRatio::SixteenByNine, + // Tall-ish → 9:16 + CommonAspectRatio::TallThreeByFour + | CommonAspectRatio::TallFourByFive + | CommonAspectRatio::TallTwoByThree + | CommonAspectRatio::TallNineByTwentyOne => Kling3p0ProMultiFunctionVideoGenAspectRatio::NineBySixteen, + // Fallback + _ => Kling3p0ProMultiFunctionVideoGenAspectRatio::Square, + } +} + +// Kling 3.0 Pro supports durations of 3–15 seconds. +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + const MIN: u16 = 3; + const MAX: u16 = 15; + match duration_seconds { + None => Ok(None), + Some(d) if d >= MIN && d <= MAX => Ok(Some(seconds_to_duration(d))), + Some(d) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", d), + })) + } + _ => Ok(Some(seconds_to_duration(d.clamp(MIN, MAX)))), + }, + } +} + +fn seconds_to_duration(seconds: u16) -> Kling3p0ProMultiFunctionVideoGenDuration { + match seconds { + 3 => Kling3p0ProMultiFunctionVideoGenDuration::ThreeSeconds, + 4 => Kling3p0ProMultiFunctionVideoGenDuration::FourSeconds, + 5 => Kling3p0ProMultiFunctionVideoGenDuration::FiveSeconds, + 6 => Kling3p0ProMultiFunctionVideoGenDuration::SixSeconds, + 7 => Kling3p0ProMultiFunctionVideoGenDuration::SevenSeconds, + 8 => Kling3p0ProMultiFunctionVideoGenDuration::EightSeconds, + 9 => Kling3p0ProMultiFunctionVideoGenDuration::NineSeconds, + 10 => Kling3p0ProMultiFunctionVideoGenDuration::TenSeconds, + 11 => Kling3p0ProMultiFunctionVideoGenDuration::ElevenSeconds, + 12 => Kling3p0ProMultiFunctionVideoGenDuration::TwelveSeconds, + 13 => Kling3p0ProMultiFunctionVideoGenDuration::ThirteenSeconds, + 14 => Kling3p0ProMultiFunctionVideoGenDuration::FourteenSeconds, + _ => Kling3p0ProMultiFunctionVideoGenDuration::FifteenSeconds, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::common_aspect_ratio::CommonAspectRatio; + use crate::api::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::errors::artcraft_router_error::ArtcraftRouterError; + use crate::errors::client_error::ClientError; + use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; + use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan; + + fn base_request() -> GenerateVideoRequest<'static> { + GenerateVideoRequest { + model: CommonVideoModel::Kling3p0Pro, + provider: Provider::Artcraft, + prompt: Some("a cat in space"), + start_frame: None, + end_frame: None, + reference_images: None, + resolution: None, + aspect_ratio: None, + duration_seconds: None, + video_batch_count: None, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + idempotency_token: None, + } + } + + #[test] + fn aspect_ratio_direct_16x9() { + let request = GenerateVideoRequest { + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + ..base_request() + }; + let VideoGenerationPlan::ArtcraftKling3p0Pro(plan) = request.build().unwrap() else { panic!("wrong variant") }; + assert!(matches!(plan.aspect_ratio, Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::SixteenByNine))); + } + + #[test] + fn aspect_ratio_direct_9x16() { + let request = GenerateVideoRequest { + aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen), + ..base_request() + }; + let VideoGenerationPlan::ArtcraftKling3p0Pro(plan) = request.build().unwrap() else { panic!("wrong variant") }; + assert!(matches!(plan.aspect_ratio, Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::NineBySixteen))); + } + + #[test] + fn aspect_ratio_direct_square() { + let request = GenerateVideoRequest { + aspect_ratio: Some(CommonAspectRatio::Square), + ..base_request() + }; + let VideoGenerationPlan::ArtcraftKling3p0Pro(plan) = request.build().unwrap() else { panic!("wrong variant") }; + assert!(matches!(plan.aspect_ratio, Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::Square))); + } + + #[test] + fn aspect_ratio_error_out_on_unsupported() { + let request = GenerateVideoRequest { + aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + ..base_request() + }; + let result = request.build(); + assert!(matches!( + result, + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. })) + )); + } + + #[test] + fn duration_valid_range() { + for d in 3..=15 { + let request = GenerateVideoRequest { + duration_seconds: Some(d), + ..base_request() + }; + let result = request.build(); + assert!(result.is_ok(), "duration {} should be valid", d); + } + } + + #[test] + fn duration_out_of_range_error_out() { + let request = GenerateVideoRequest { + duration_seconds: Some(16), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + ..base_request() + }; + let result = request.build(); + assert!(matches!( + result, + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. })) + )); + } + + #[test] + fn url_image_ref_returns_error() { + let request = GenerateVideoRequest { + start_frame: Some(ImageRef::Url("https://example.com/image.jpg")), + ..base_request() + }; + let result = request.build(); + assert!(matches!( + result, + Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens)) + )); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/plan_generate_video_artcraft_kling3p0_standard.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/plan_generate_video_artcraft_kling3p0_standard.rs new file mode 100644 index 0000000000..329434bd43 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/plan/artcraft/plan_generate_video_artcraft_kling3p0_standard.rs @@ -0,0 +1,266 @@ +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::image_ref::ImageRef; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; +use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::{ + Kling3p0StandardMultiFunctionVideoGenAspectRatio, + Kling3p0StandardMultiFunctionVideoGenDuration, +}; +use tokens::tokens::media_files::MediaFileToken; + +#[derive(Debug, Clone)] +pub struct PlanArtcraftKling3p0Standard<'a> { + pub prompt: Option<&'a str>, + pub start_frame: Option<&'a MediaFileToken>, + pub end_frame: Option<&'a MediaFileToken>, + pub aspect_ratio: Option, + pub duration: Option, + pub idempotency_token: String, +} + +pub fn plan_generate_video_artcraft_kling3p0_standard<'a>( + request: &'a GenerateVideoRequest<'a>, +) -> Result, ArtcraftRouterError> { + let strategy = request.request_mismatch_mitigation_strategy; + + let start_frame = resolve_image_ref(request.start_frame)?; + let end_frame = resolve_image_ref(request.end_frame)?; + + let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?; + let duration = plan_duration(request.duration_seconds, strategy)?; + + Ok(PlanArtcraftKling3p0Standard { + prompt: request.prompt, + start_frame, + end_frame, + aspect_ratio, + duration, + idempotency_token: request.get_or_generate_idempotency_token(), + }) +} + +fn resolve_image_ref<'a>( + image_ref: Option>, +) -> Result, ArtcraftRouterError> { + match image_ref { + None => Ok(None), + Some(ImageRef::MediaFileToken(t)) => Ok(Some(t)), + Some(ImageRef::Url(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens)) + } + } +} + +// Kling 3.0 Standard supports: Square (1:1), SixteenByNine (16:9), NineBySixteen (9:16) +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match aspect_ratio { + None => Ok(None), + + // Direct mappings + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => { + Ok(Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::SixteenByNine)) + } + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => { + Ok(Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::NineBySixteen)) + } + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => { + Ok(Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::Square)) + } + + // Auto defaults to 16:9 + Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => { + Ok(Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::SixteenByNine)) + } + + // Mismatches + Some(unsupported) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade + | RequestMismatchMitigationStrategy::PayLessDowngrade => { + Ok(Some(nearest_aspect_ratio(unsupported))) + } + }, + } +} + +fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> Kling3p0StandardMultiFunctionVideoGenAspectRatio { + match aspect_ratio { + // Wide-ish → 16:9 + CommonAspectRatio::WideFourByThree + | CommonAspectRatio::WideFiveByFour + | CommonAspectRatio::WideThreeByTwo + | CommonAspectRatio::WideTwentyOneByNine => Kling3p0StandardMultiFunctionVideoGenAspectRatio::SixteenByNine, + // Tall-ish → 9:16 + CommonAspectRatio::TallThreeByFour + | CommonAspectRatio::TallFourByFive + | CommonAspectRatio::TallTwoByThree + | CommonAspectRatio::TallNineByTwentyOne => Kling3p0StandardMultiFunctionVideoGenAspectRatio::NineBySixteen, + // Fallback + _ => Kling3p0StandardMultiFunctionVideoGenAspectRatio::Square, + } +} + +// Kling 3.0 Standard supports durations of 3–15 seconds. +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + const MIN: u16 = 3; + const MAX: u16 = 15; + match duration_seconds { + None => Ok(None), + Some(d) if d >= MIN && d <= MAX => Ok(Some(seconds_to_duration(d))), + Some(d) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", d), + })) + } + _ => Ok(Some(seconds_to_duration(d.clamp(MIN, MAX)))), + }, + } +} + +fn seconds_to_duration(seconds: u16) -> Kling3p0StandardMultiFunctionVideoGenDuration { + match seconds { + 3 => Kling3p0StandardMultiFunctionVideoGenDuration::ThreeSeconds, + 4 => Kling3p0StandardMultiFunctionVideoGenDuration::FourSeconds, + 5 => Kling3p0StandardMultiFunctionVideoGenDuration::FiveSeconds, + 6 => Kling3p0StandardMultiFunctionVideoGenDuration::SixSeconds, + 7 => Kling3p0StandardMultiFunctionVideoGenDuration::SevenSeconds, + 8 => Kling3p0StandardMultiFunctionVideoGenDuration::EightSeconds, + 9 => Kling3p0StandardMultiFunctionVideoGenDuration::NineSeconds, + 10 => Kling3p0StandardMultiFunctionVideoGenDuration::TenSeconds, + 11 => Kling3p0StandardMultiFunctionVideoGenDuration::ElevenSeconds, + 12 => Kling3p0StandardMultiFunctionVideoGenDuration::TwelveSeconds, + 13 => Kling3p0StandardMultiFunctionVideoGenDuration::ThirteenSeconds, + 14 => Kling3p0StandardMultiFunctionVideoGenDuration::FourteenSeconds, + _ => Kling3p0StandardMultiFunctionVideoGenDuration::FifteenSeconds, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::common_aspect_ratio::CommonAspectRatio; + use crate::api::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::errors::artcraft_router_error::ArtcraftRouterError; + use crate::errors::client_error::ClientError; + use crate::generate::generate_video::generate_video_request::GenerateVideoRequest; + use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan; + + fn base_request() -> GenerateVideoRequest<'static> { + GenerateVideoRequest { + model: CommonVideoModel::Kling3p0Standard, + provider: Provider::Artcraft, + prompt: Some("a cat in space"), + start_frame: None, + end_frame: None, + reference_images: None, + resolution: None, + aspect_ratio: None, + duration_seconds: None, + video_batch_count: None, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + idempotency_token: None, + } + } + + #[test] + fn aspect_ratio_direct_16x9() { + let request = GenerateVideoRequest { + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + ..base_request() + }; + let VideoGenerationPlan::ArtcraftKling3p0Standard(plan) = request.build().unwrap() else { panic!("wrong variant") }; + assert!(matches!(plan.aspect_ratio, Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::SixteenByNine))); + } + + #[test] + fn aspect_ratio_direct_9x16() { + let request = GenerateVideoRequest { + aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen), + ..base_request() + }; + let VideoGenerationPlan::ArtcraftKling3p0Standard(plan) = request.build().unwrap() else { panic!("wrong variant") }; + assert!(matches!(plan.aspect_ratio, Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::NineBySixteen))); + } + + #[test] + fn aspect_ratio_direct_square() { + let request = GenerateVideoRequest { + aspect_ratio: Some(CommonAspectRatio::Square), + ..base_request() + }; + let VideoGenerationPlan::ArtcraftKling3p0Standard(plan) = request.build().unwrap() else { panic!("wrong variant") }; + assert!(matches!(plan.aspect_ratio, Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::Square))); + } + + #[test] + fn aspect_ratio_error_out_on_unsupported() { + let request = GenerateVideoRequest { + aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + ..base_request() + }; + let result = request.build(); + assert!(matches!( + result, + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. })) + )); + } + + #[test] + fn duration_valid_range() { + for d in 3..=15 { + let request = GenerateVideoRequest { + duration_seconds: Some(d), + ..base_request() + }; + let result = request.build(); + assert!(result.is_ok(), "duration {} should be valid", d); + } + } + + #[test] + fn duration_out_of_range_error_out() { + let request = GenerateVideoRequest { + duration_seconds: Some(16), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + ..base_request() + }; + let result = request.build(); + assert!(matches!( + result, + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. })) + )); + } + + #[test] + fn url_image_ref_returns_error() { + let request = GenerateVideoRequest { + start_frame: Some(ImageRef::Url("https://example.com/image.jpg")), + ..base_request() + }; + let result = request.build(); + assert!(matches!( + result, + Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens)) + )); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_plan.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_plan.rs index 608769572f..b59be58be0 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_plan.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/video_generation_plan.rs @@ -1,16 +1,24 @@ use crate::client::router_client::RouterClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_kling3p0_pro::estimate_video_cost_artcraft_kling3p0_pro; +use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_kling3p0_standard::estimate_video_cost_artcraft_kling3p0_standard; use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance1p5_pro::estimate_video_cost_artcraft_seedance1p5_pro; use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance2p0::estimate_video_cost_artcraft_seedance2p0; +use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling3p0_pro::execute_artcraft_kling3p0_pro; +use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_kling3p0_standard::execute_artcraft_kling3p0_standard; use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance1p5_pro::execute_artcraft_seedance1p5_pro; use crate::generate::generate_video::execute::artcraft::generate_video_artcraft_seedance2p0::execute_artcraft_seedance2p0; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; +use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::PlanArtcraftKling3p0Pro; +use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::PlanArtcraftKling3p0Standard; use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::PlanArtcraftSeedance1p5Pro; use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0; use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; #[derive(Debug)] pub enum VideoGenerationPlan<'a> { + ArtcraftKling3p0Pro(PlanArtcraftKling3p0Pro<'a>), + ArtcraftKling3p0Standard(PlanArtcraftKling3p0Standard<'a>), ArtcraftSeedance1p5Pro(PlanArtcraftSeedance1p5Pro<'a>), ArtcraftSeedance2p0(PlanArtcraftSeedance2p0<'a>), } @@ -21,6 +29,14 @@ impl<'a> VideoGenerationPlan<'a> { client: &RouterClient, ) -> Result { match self { + VideoGenerationPlan::ArtcraftKling3p0Pro(plan) => { + let artcraft_client = client.get_artcraft_client_ref()?; + execute_artcraft_kling3p0_pro(plan, artcraft_client).await + } + VideoGenerationPlan::ArtcraftKling3p0Standard(plan) => { + let artcraft_client = client.get_artcraft_client_ref()?; + execute_artcraft_kling3p0_standard(plan, artcraft_client).await + } VideoGenerationPlan::ArtcraftSeedance1p5Pro(plan) => { let artcraft_client = client.get_artcraft_client_ref()?; execute_artcraft_seedance1p5_pro(plan, artcraft_client).await @@ -34,6 +50,12 @@ impl<'a> VideoGenerationPlan<'a> { pub fn estimate_costs(&self) -> VideoGenerationCostEstimate { match self { + VideoGenerationPlan::ArtcraftKling3p0Pro(plan) => { + estimate_video_cost_artcraft_kling3p0_pro(plan) + } + VideoGenerationPlan::ArtcraftKling3p0Standard(plan) => { + estimate_video_cost_artcraft_kling3p0_standard(plan) + } VideoGenerationPlan::ArtcraftSeedance1p5Pro(plan) => { estimate_video_cost_artcraft_seedance1p5_pro(plan) } diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v3_pro_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v3_pro_image_to_video_webhook.rs new file mode 100644 index 0000000000..200fe33c24 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v3_pro_image_to_video_webhook.rs @@ -0,0 +1,105 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; +use crate::requests::webhook::video::text::enqueue_kling_v3_pro_text_to_video_webhook::EnqueueKlingV3ProTextToVideoDuration; +use fal::endpoints::fal_ai::kling_video::v3::kling_v3_pro_image_to_video::{kling_v3_pro_image_to_video, KlingV3ProImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueKlingV3ProImageToVideoArgs<'a, R: IntoUrl> { + pub prompt: String, + pub image_url: String, + + // Optional args + pub end_image_url: Option, + pub generate_audio: Option, + pub negative_prompt: Option, + pub duration: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +impl FalRequestCostCalculator for EnqueueKlingV3ProImageToVideoArgs<'_, U> { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Same pricing as text-to-video for Kling 3.0 Pro: + // Audio off: $0.224/second + // Audio on: $0.336/second + let generate_audio = self.generate_audio.unwrap_or(true); + let duration_secs = self.duration + .unwrap_or(EnqueueKlingV3ProTextToVideoDuration::FiveSeconds) + .to_seconds(); + + let rate = if generate_audio { 336u64 } else { 224u64 }; + (rate * duration_secs + 9) / 10 + } +} + +/// Kling 3.0 Pro Image-to-Video +/// https://fal.ai/models/fal-ai/kling-video/v3/pro/image-to-video +pub async fn enqueue_kling_v3_pro_image_to_video_webhook( + args: EnqueueKlingV3ProImageToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|d| d.to_str().to_string()); + + let request = KlingV3ProImageToVideoInput { + prompt: args.prompt, + image_url: args.image_url, + end_image_url: args.end_image_url, + aspect_ratio: None, + generate_audio: args.generate_audio, + duration, + negative_prompt: args.negative_prompt, + cfg_scale: None, + }; + + let result = kling_v3_pro_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cost_pro_i2v_audio_off_5s() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3ProImageToVideoArgs { + prompt: String::new(), + image_url: String::new(), + end_image_url: None, + generate_audio: Some(false), + negative_prompt: None, + duration: Some(EnqueueKlingV3ProTextToVideoDuration::FiveSeconds), + webhook_url: "https://example.com", + api_key: &api_key, + }; + // $0.224 * 5 = $1.12 = 112 cents + assert_eq!(args.calculate_cost_in_cents(), 112); + } + + #[test] + fn test_cost_pro_i2v_audio_on_10s() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3ProImageToVideoArgs { + prompt: String::new(), + image_url: String::new(), + end_image_url: None, + generate_audio: Some(true), + negative_prompt: None, + duration: Some(EnqueueKlingV3ProTextToVideoDuration::TenSeconds), + webhook_url: "https://example.com", + api_key: &api_key, + }; + // $0.336 * 10 = $3.36 = 336 cents + assert_eq!(args.calculate_cost_in_cents(), 336); + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v3_standard_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v3_standard_image_to_video_webhook.rs new file mode 100644 index 0000000000..d65d3b3e04 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v3_standard_image_to_video_webhook.rs @@ -0,0 +1,104 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; +use crate::requests::webhook::video::text::enqueue_kling_v3_standard_text_to_video_webhook::EnqueueKlingV3StandardTextToVideoDuration; +use fal::endpoints::fal_ai::kling_video::v3::kling_v3_standard_image_to_video::{kling_v3_standard_image_to_video, KlingV3StandardImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueKlingV3StandardImageToVideoArgs<'a, R: IntoUrl> { + pub prompt: String, + pub image_url: String, + + // Optional args + pub end_image_url: Option, + pub generate_audio: Option, + pub negative_prompt: Option, + pub duration: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +impl FalRequestCostCalculator for EnqueueKlingV3StandardImageToVideoArgs<'_, U> { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Same pricing as text-to-video for Kling 3.0 Standard: + // Audio off: $0.168/second + // Audio on: $0.252/second + let generate_audio = self.generate_audio.unwrap_or(true); + let duration_secs = self.duration + .unwrap_or(EnqueueKlingV3StandardTextToVideoDuration::FiveSeconds) + .to_seconds(); + + let rate = if generate_audio { 252u64 } else { 168u64 }; + (rate * duration_secs + 9) / 10 + } +} + +/// Kling 3.0 Standard Image-to-Video +/// https://fal.ai/models/fal-ai/kling-video/v3/standard/image-to-video +pub async fn enqueue_kling_v3_standard_image_to_video_webhook( + args: EnqueueKlingV3StandardImageToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|d| d.to_str().to_string()); + + let request = KlingV3StandardImageToVideoInput { + prompt: args.prompt, + image_url: args.image_url, + end_image_url: args.end_image_url, + aspect_ratio: None, // Not applicable for image-to-video + generate_audio: args.generate_audio, + duration, + negative_prompt: args.negative_prompt, + cfg_scale: None, + }; + + let result = kling_v3_standard_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cost_standard_i2v_audio_off_5s() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3StandardImageToVideoArgs { + prompt: String::new(), + image_url: String::new(), + end_image_url: None, + generate_audio: Some(false), + negative_prompt: None, + duration: Some(EnqueueKlingV3StandardTextToVideoDuration::FiveSeconds), + webhook_url: "https://example.com", + api_key: &api_key, + }; + assert_eq!(args.calculate_cost_in_cents(), 84); + } + + #[test] + fn test_cost_standard_i2v_audio_on_10s() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3StandardImageToVideoArgs { + prompt: String::new(), + image_url: String::new(), + end_image_url: None, + generate_audio: Some(true), + negative_prompt: None, + duration: Some(EnqueueKlingV3StandardTextToVideoDuration::TenSeconds), + webhook_url: "https://example.com", + api_key: &api_key, + }; + // $0.252 * 10 = $2.52 = 252 cents + assert_eq!(args.calculate_cost_in_cents(), 252); + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/mod.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/mod.rs index 6969114287..7c7696e762 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/video/image/mod.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/mod.rs @@ -6,6 +6,8 @@ pub mod enqueue_kling_v2p1_pro_image_to_video_webhook; pub mod enqueue_kling_v2p5_turbo_pro_image_to_video_webhook; pub mod enqueue_kling_v2p5_turbo_standard_image_to_video_webhook; pub mod enqueue_kling_v2p6_pro_image_to_video_webhook; +pub mod enqueue_kling_v3_pro_image_to_video_webhook; +pub mod enqueue_kling_v3_standard_image_to_video_webhook; pub mod enqueue_pixverse_v5_image_to_video_webhook; pub mod enqueue_seedance_1_lite_image_to_video_webhook; pub mod enqueue_seedance_1_pro_image_to_video_webhook; diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v3_pro_text_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v3_pro_text_to_video_webhook.rs new file mode 100644 index 0000000000..dbc178711a --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v3_pro_text_to_video_webhook.rs @@ -0,0 +1,201 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; +use fal::endpoints::fal_ai::kling_video::v3::kling_v3_pro_text_to_video::{kling_v3_pro_text_to_video, KlingV3ProTextToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueKlingV3ProTextToVideoArgs<'a, R: IntoUrl> { + pub prompt: String, + + // Optional args + pub generate_audio: Option, + pub negative_prompt: Option, + pub duration: Option, + pub aspect_ratio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueKlingV3ProTextToVideoDuration { + ThreeSeconds, + FourSeconds, + FiveSeconds, + SixSeconds, + SevenSeconds, + EightSeconds, + NineSeconds, + TenSeconds, + ElevenSeconds, + TwelveSeconds, + ThirteenSeconds, + FourteenSeconds, + FifteenSeconds, +} + +impl EnqueueKlingV3ProTextToVideoDuration { + pub fn to_seconds(&self) -> u64 { + match self { + Self::ThreeSeconds => 3, + Self::FourSeconds => 4, + Self::FiveSeconds => 5, + Self::SixSeconds => 6, + Self::SevenSeconds => 7, + Self::EightSeconds => 8, + Self::NineSeconds => 9, + Self::TenSeconds => 10, + Self::ElevenSeconds => 11, + Self::TwelveSeconds => 12, + Self::ThirteenSeconds => 13, + Self::FourteenSeconds => 14, + Self::FifteenSeconds => 15, + } + } + + pub fn to_str(&self) -> &'static str { + match self { + Self::ThreeSeconds => "3", + Self::FourSeconds => "4", + Self::FiveSeconds => "5", + Self::SixSeconds => "6", + Self::SevenSeconds => "7", + Self::EightSeconds => "8", + Self::NineSeconds => "9", + Self::TenSeconds => "10", + Self::ElevenSeconds => "11", + Self::TwelveSeconds => "12", + Self::ThirteenSeconds => "13", + Self::FourteenSeconds => "14", + Self::FifteenSeconds => "15", + } + } +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueKlingV3ProTextToVideoAspectRatio { + Square, + SixteenByNine, + NineBySixteen, +} + +impl FalRequestCostCalculator for EnqueueKlingV3ProTextToVideoArgs<'_, U> { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Kling 3.0 Pro pricing: + // Audio off: $0.224/second + // Audio on: $0.336/second + let generate_audio = self.generate_audio.unwrap_or(true); + let duration_secs = self.duration + .unwrap_or(EnqueueKlingV3ProTextToVideoDuration::FiveSeconds) + .to_seconds(); + + let rate = if generate_audio { 336u64 } else { 224u64 }; + (rate * duration_secs + 9) / 10 + } +} + +/// Kling 3.0 Pro Text-to-Video +/// https://fal.ai/models/fal-ai/kling-video/v3/pro/text-to-video +pub async fn enqueue_kling_v3_pro_text_to_video_webhook( + args: EnqueueKlingV3ProTextToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|d| d.to_str().to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect| match aspect { + EnqueueKlingV3ProTextToVideoAspectRatio::Square => "1:1", + EnqueueKlingV3ProTextToVideoAspectRatio::SixteenByNine => "16:9", + EnqueueKlingV3ProTextToVideoAspectRatio::NineBySixteen => "9:16", + }) + .map(|s| s.to_string()); + + let request = KlingV3ProTextToVideoInput { + prompt: args.prompt, + generate_audio: args.generate_audio, + duration, + aspect_ratio, + negative_prompt: args.negative_prompt, + cfg_scale: None, + }; + + let result = kling_v3_pro_text_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cost_pro_audio_off_5s() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3ProTextToVideoArgs { + prompt: String::new(), + generate_audio: Some(false), + negative_prompt: None, + duration: Some(EnqueueKlingV3ProTextToVideoDuration::FiveSeconds), + aspect_ratio: None, + webhook_url: "https://example.com", + api_key: &api_key, + }; + // $0.224 * 5 = $1.12 = 112 cents + assert_eq!(args.calculate_cost_in_cents(), 112); + } + + #[test] + fn test_cost_pro_audio_on_5s() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3ProTextToVideoArgs { + prompt: String::new(), + generate_audio: Some(true), + negative_prompt: None, + duration: Some(EnqueueKlingV3ProTextToVideoDuration::FiveSeconds), + aspect_ratio: None, + webhook_url: "https://example.com", + api_key: &api_key, + }; + // $0.336 * 5 = $1.68 = 168 cents + assert_eq!(args.calculate_cost_in_cents(), 168); + } + + #[test] + fn test_cost_pro_audio_off_10s() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3ProTextToVideoArgs { + prompt: String::new(), + generate_audio: Some(false), + negative_prompt: None, + duration: Some(EnqueueKlingV3ProTextToVideoDuration::TenSeconds), + aspect_ratio: None, + webhook_url: "https://example.com", + api_key: &api_key, + }; + // $0.224 * 10 = $2.24 = 224 cents + assert_eq!(args.calculate_cost_in_cents(), 224); + } + + #[test] + fn test_cost_pro_audio_off_15s() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3ProTextToVideoArgs { + prompt: String::new(), + generate_audio: Some(false), + negative_prompt: None, + duration: Some(EnqueueKlingV3ProTextToVideoDuration::FifteenSeconds), + aspect_ratio: None, + webhook_url: "https://example.com", + api_key: &api_key, + }; + // $0.224 * 15 = $3.36 = 336 cents + assert_eq!(args.calculate_cost_in_cents(), 336); + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v3_standard_text_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v3_standard_text_to_video_webhook.rs new file mode 100644 index 0000000000..434dcee116 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v3_standard_text_to_video_webhook.rs @@ -0,0 +1,218 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; +use fal::endpoints::fal_ai::kling_video::v3::kling_v3_standard_text_to_video::{kling_v3_standard_text_to_video, KlingV3StandardTextToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueKlingV3StandardTextToVideoArgs<'a, R: IntoUrl> { + pub prompt: String, + + // Optional args + pub generate_audio: Option, + pub negative_prompt: Option, + pub duration: Option, + pub aspect_ratio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueKlingV3StandardTextToVideoDuration { + ThreeSeconds, + FourSeconds, + FiveSeconds, + SixSeconds, + SevenSeconds, + EightSeconds, + NineSeconds, + TenSeconds, + ElevenSeconds, + TwelveSeconds, + ThirteenSeconds, + FourteenSeconds, + FifteenSeconds, +} + +impl EnqueueKlingV3StandardTextToVideoDuration { + pub fn to_seconds(&self) -> u64 { + match self { + Self::ThreeSeconds => 3, + Self::FourSeconds => 4, + Self::FiveSeconds => 5, + Self::SixSeconds => 6, + Self::SevenSeconds => 7, + Self::EightSeconds => 8, + Self::NineSeconds => 9, + Self::TenSeconds => 10, + Self::ElevenSeconds => 11, + Self::TwelveSeconds => 12, + Self::ThirteenSeconds => 13, + Self::FourteenSeconds => 14, + Self::FifteenSeconds => 15, + } + } + + pub fn to_str(&self) -> &'static str { + match self { + Self::ThreeSeconds => "3", + Self::FourSeconds => "4", + Self::FiveSeconds => "5", + Self::SixSeconds => "6", + Self::SevenSeconds => "7", + Self::EightSeconds => "8", + Self::NineSeconds => "9", + Self::TenSeconds => "10", + Self::ElevenSeconds => "11", + Self::TwelveSeconds => "12", + Self::ThirteenSeconds => "13", + Self::FourteenSeconds => "14", + Self::FifteenSeconds => "15", + } + } +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueKlingV3StandardTextToVideoAspectRatio { + Square, + SixteenByNine, + NineBySixteen, +} + +impl FalRequestCostCalculator for EnqueueKlingV3StandardTextToVideoArgs<'_, U> { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Kling 3.0 Standard pricing: + // Audio off: $0.168/second + // Audio on: $0.252/second + let generate_audio = self.generate_audio.unwrap_or(true); + let duration_secs = self.duration + .unwrap_or(EnqueueKlingV3StandardTextToVideoDuration::FiveSeconds) + .to_seconds(); + + // Rate in tenths-of-cents per second + let rate = if generate_audio { 252u64 } else { 168u64 }; + (rate * duration_secs + 9) / 10 + } +} + +/// Kling 3.0 Standard Text-to-Video +/// https://fal.ai/models/fal-ai/kling-video/v3/standard/text-to-video +pub async fn enqueue_kling_v3_standard_text_to_video_webhook( + args: EnqueueKlingV3StandardTextToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|d| d.to_str().to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect| match aspect { + EnqueueKlingV3StandardTextToVideoAspectRatio::Square => "1:1", + EnqueueKlingV3StandardTextToVideoAspectRatio::SixteenByNine => "16:9", + EnqueueKlingV3StandardTextToVideoAspectRatio::NineBySixteen => "9:16", + }) + .map(|s| s.to_string()); + + let request = KlingV3StandardTextToVideoInput { + prompt: args.prompt, + generate_audio: args.generate_audio, + duration, + aspect_ratio, + negative_prompt: args.negative_prompt, + cfg_scale: None, + }; + + let result = kling_v3_standard_text_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cost_standard_audio_off() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3StandardTextToVideoArgs { + prompt: String::new(), + generate_audio: Some(false), + negative_prompt: None, + duration: Some(EnqueueKlingV3StandardTextToVideoDuration::FiveSeconds), + aspect_ratio: None, + webhook_url: "https://example.com", + api_key: &api_key, + }; + // $0.168 * 5 = $0.84 = 84 cents + assert_eq!(args.calculate_cost_in_cents(), 84); + } + + #[test] + fn test_cost_standard_audio_on() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3StandardTextToVideoArgs { + prompt: String::new(), + generate_audio: Some(true), + negative_prompt: None, + duration: Some(EnqueueKlingV3StandardTextToVideoDuration::FiveSeconds), + aspect_ratio: None, + webhook_url: "https://example.com", + api_key: &api_key, + }; + // $0.252 * 5 = $1.26 = 126 cents + assert_eq!(args.calculate_cost_in_cents(), 126); + } + + #[test] + fn test_cost_standard_audio_off_10s() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3StandardTextToVideoArgs { + prompt: String::new(), + generate_audio: Some(false), + negative_prompt: None, + duration: Some(EnqueueKlingV3StandardTextToVideoDuration::TenSeconds), + aspect_ratio: None, + webhook_url: "https://example.com", + api_key: &api_key, + }; + // $0.168 * 10 = $1.68 = 168 cents + assert_eq!(args.calculate_cost_in_cents(), 168); + } + + #[test] + fn test_cost_standard_audio_off_3s() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3StandardTextToVideoArgs { + prompt: String::new(), + generate_audio: Some(false), + negative_prompt: None, + duration: Some(EnqueueKlingV3StandardTextToVideoDuration::ThreeSeconds), + aspect_ratio: None, + webhook_url: "https://example.com", + api_key: &api_key, + }; + // $0.168 * 3 = $0.504 = ceil(50.4) = 51 cents + assert_eq!(args.calculate_cost_in_cents(), 51); + } + + #[test] + fn test_cost_standard_audio_off_15s() { + let api_key = FalApiKey::from_str(""); + let args = EnqueueKlingV3StandardTextToVideoArgs { + prompt: String::new(), + generate_audio: Some(false), + negative_prompt: None, + duration: Some(EnqueueKlingV3StandardTextToVideoDuration::FifteenSeconds), + aspect_ratio: None, + webhook_url: "https://example.com", + api_key: &api_key, + }; + // $0.168 * 15 = $2.52 = 252 cents + assert_eq!(args.calculate_cost_in_cents(), 252); + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/mod.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/mod.rs index 839bb3b485..ec0b9dd584 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/video/text/mod.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/mod.rs @@ -1,6 +1,8 @@ pub mod enqueue_hailuo_v2p3_pro_text_to_video_webhook; pub mod enqueue_kling_v2p5_turbo_pro_text_to_video_webhook; pub mod enqueue_kling_v2p6_pro_text_to_video_webhook; +pub mod enqueue_kling_v3_pro_text_to_video_webhook; +pub mod enqueue_kling_v3_standard_text_to_video_webhook; pub mod enqueue_pixverse_v5_text_to_video_webhook; pub mod enqueue_seedance_1p5_pro_text_to_video_webhook; pub mod enqueue_sora_2_pro_text_to_video_webhook; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/handle_artcraft_kling_3p0_pro.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/handle_artcraft_kling_3p0_pro.rs new file mode 100644 index 0000000000..1704e57301 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/handle_artcraft_kling_3p0_pro.rs @@ -0,0 +1,71 @@ +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_kling_3p0_pro( + request: &EnqueueImageToVideoRequest, + app_env_configs: &AppEnvConfigs, + creds: &StorytellerCredentialSet, +) -> Result { + 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::Kling3p0Pro, + 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::Kling3p0Pro), + provider: GenerationProvider::Artcraft, + provider_job_id: Some(job_id), + }) +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/handle_artcraft_kling_3p0_standard.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/handle_artcraft_kling_3p0_standard.rs new file mode 100644 index 0000000000..237b39423b --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/handle_artcraft_kling_3p0_standard.rs @@ -0,0 +1,71 @@ +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_kling_3p0_standard( + request: &EnqueueImageToVideoRequest, + app_env_configs: &AppEnvConfigs, + creds: &StorytellerCredentialSet, +) -> Result { + 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::Kling3p0Standard, + 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::Kling3p0Standard), + provider: GenerationProvider::Artcraft, + provider_job_id: Some(job_id), + }) +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/handle_artcraft_video.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/handle_artcraft_video.rs index 4bf3b94fba..80cd85a247 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/handle_artcraft_video.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/handle_artcraft_video.rs @@ -5,6 +5,8 @@ use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kl use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_2p1_pro::handle_artcraft_kling_2p1_pro; use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_2p5_turbo_pro::handle_artcraft_kling_2p5_turbo_pro; use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_2p6_pro::handle_artcraft_kling_2p6_pro; +use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_3p0_pro::handle_artcraft_kling_3p0_pro; +use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_kling_3p0_standard::handle_artcraft_kling_3p0_standard; use crate::core::commands::enqueue::image_to_video::artcraft::handle_artcraft_seedance_1_lite::handle_artcraft_seedance_1_lite; 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; @@ -40,6 +42,8 @@ pub async fn handle_video_artcraft( Some(VideoModel::Kling21Master) => handle_artcraft_kling_2p1_master(request, app_env_configs, &creds).await, Some(VideoModel::Kling2p5TurboPro) => handle_artcraft_kling_2p5_turbo_pro(request, app_env_configs, &creds).await, Some(VideoModel::Kling2p6Pro) => handle_artcraft_kling_2p6_pro(request, app_env_configs, &creds).await, + Some(VideoModel::Kling3p0Standard) => handle_artcraft_kling_3p0_standard(request, app_env_configs, &creds).await, + Some(VideoModel::Kling3p0Pro) => handle_artcraft_kling_3p0_pro(request, app_env_configs, &creds).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::Seedance2p0) => handle_artcraft_seedance_2p0(request, app_env_configs, &creds).await, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/mod.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/mod.rs index 0f6b46b3d7..3ef6048195 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/mod.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/artcraft/mod.rs @@ -4,6 +4,8 @@ mod handle_artcraft_kling_2p1_master; mod handle_artcraft_kling_2p1_pro; mod handle_artcraft_kling_2p5_turbo_pro; mod handle_artcraft_kling_2p6_pro; +mod handle_artcraft_kling_3p0_pro; +mod handle_artcraft_kling_3p0_standard; mod handle_artcraft_seedance_1_lite; mod handle_artcraft_seedance_1p5_pro; mod handle_artcraft_seedance_2p0; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs index 6aa42259ba..c426bbc14c 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs @@ -54,6 +54,12 @@ pub enum VideoModel { #[serde(rename = "kling_2p6_pro")] Kling2p6Pro, + #[serde(rename = "kling_3p0_standard")] + Kling3p0Standard, + + #[serde(rename = "kling_3p0_pro")] + Kling3p0Pro, + #[serde(rename = "seedance_1.0_lite")] Seedance10Lite, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/task_enqueue_success.rs b/crates/desktop/artcraft/src/core/commands/enqueue/task_enqueue_success.rs index fec6ab8f60..26632bdc59 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/task_enqueue_success.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/task_enqueue_success.rs @@ -83,6 +83,8 @@ impl TaskEnqueueSuccess{ Some(GenerationModel::Kling21Master) => Some(TaskModelType::Kling21Master), Some(GenerationModel::Kling2p5TurboPro) => Some(TaskModelType::Kling2p5TurboPro), Some(GenerationModel::Kling2p6Pro) => Some(TaskModelType::Kling2p6Pro), + Some(GenerationModel::Kling3p0Standard) => Some(TaskModelType::Kling3p0Standard), + Some(GenerationModel::Kling3p0Pro) => Some(TaskModelType::Kling3p0Pro), Some(GenerationModel::Seedance10Lite) => Some(TaskModelType::Seedance10Lite), Some(GenerationModel::Seedance1p5Pro) => Some(TaskModelType::Seedance1p5Pro), Some(GenerationModel::Seedance2p0) => Some(TaskModelType::Seedance2p0), diff --git a/crates/desktop/artcraft/src/core/events/generation_events/common.rs b/crates/desktop/artcraft/src/core/events/generation_events/common.rs index 0cb5120538..6f3dd4bcb1 100644 --- a/crates/desktop/artcraft/src/core/events/generation_events/common.rs +++ b/crates/desktop/artcraft/src/core/events/generation_events/common.rs @@ -80,6 +80,10 @@ pub enum GenerationModel { Kling2p5TurboPro, #[serde(rename = "kling_2p6_pro")] Kling2p6Pro, + #[serde(rename = "kling_3p0_standard")] + Kling3p0Standard, + #[serde(rename = "kling_3p0_pro")] + Kling3p0Pro, #[serde(rename = "recraft_3")] Recraft3, #[serde(rename = "sora")] diff --git a/crates/schema/public/enums/src/common/generation/common_video_model.rs b/crates/schema/public/enums/src/common/generation/common_video_model.rs index 028c6a60be..2902d5f3f5 100644 --- a/crates/schema/public/enums/src/common/generation/common_video_model.rs +++ b/crates/schema/public/enums/src/common/generation/common_video_model.rs @@ -23,6 +23,12 @@ pub enum CommonVideoModel { #[serde(rename = "kling_2p6_pro")] Kling2p6Pro, + #[serde(rename = "kling_3p0_standard")] + Kling3p0Standard, + + #[serde(rename = "kling_3p0_pro")] + Kling3p0Pro, + #[serde(rename = "seedance_1p0_lite")] Seedance10Lite, diff --git a/crates/schema/public/enums/src/common/model_type.rs b/crates/schema/public/enums/src/common/model_type.rs index 705a95ddf2..d7b554b17c 100644 --- a/crates/schema/public/enums/src/common/model_type.rs +++ b/crates/schema/public/enums/src/common/model_type.rs @@ -93,6 +93,10 @@ pub enum ModelType { Kling2p5TurboPro, #[serde(rename = "kling_2p6_pro")] Kling2p6Pro, + #[serde(rename = "kling_3p0_standard")] + Kling3p0Standard, + #[serde(rename = "kling_3p0_pro")] + Kling3p0Pro, #[serde(rename = "seedance_1p0_lite")] Seedance10Lite, #[serde(rename = "seedance_1p0_pro")] @@ -171,6 +175,8 @@ impl ModelType { Self::Kling21Master => "kling_2p1_master", Self::Kling2p5TurboPro => "kling_2p5_turbo_pro", Self::Kling2p6Pro => "kling_2p6_pro", + Self::Kling3p0Standard => "kling_3p0_standard", + Self::Kling3p0Pro => "kling_3p0_pro", Self::Seedance10Lite => "seedance_1p0_lite", Self::Seedance10Pro => "seedance_1p0_pro", Self::Seedance1p5Pro => "seedance_1p5_pro", @@ -229,6 +235,8 @@ impl ModelType { "kling_2p1_master" => Ok(Self::Kling21Master), "kling_2p5_turbo_pro" => Ok(Self::Kling2p5TurboPro), "kling_2p6_pro" => Ok(Self::Kling2p6Pro), + "kling_3p0_standard" => Ok(Self::Kling3p0Standard), + "kling_3p0_pro" => Ok(Self::Kling3p0Pro), "seedance_1p0_lite" => Ok(Self::Seedance10Lite), "seedance_1p0_pro" => Ok(Self::Seedance10Pro), "seedance_1p5_pro" => Ok(Self::Seedance1p5Pro), @@ -291,6 +299,8 @@ impl ModelType { Self::Kling21Master, Self::Kling2p5TurboPro, Self::Kling2p6Pro, + Self::Kling3p0Standard, + Self::Kling3p0Pro, Self::Seedance10Lite, Self::Seedance10Pro, Self::Seedance1p5Pro, @@ -357,6 +367,8 @@ mod tests { assert_serialization(ModelType::Kling21Master, "kling_2p1_master"); assert_serialization(ModelType::Kling2p5TurboPro, "kling_2p5_turbo_pro"); assert_serialization(ModelType::Kling2p6Pro, "kling_2p6_pro"); + assert_serialization(ModelType::Kling3p0Standard, "kling_3p0_standard"); + assert_serialization(ModelType::Kling3p0Pro, "kling_3p0_pro"); assert_serialization(ModelType::Seedance10Lite, "seedance_1p0_lite"); assert_serialization(ModelType::Seedance10Pro, "seedance_1p0_pro"); assert_serialization(ModelType::Seedance1p5Pro, "seedance_1p5_pro"); @@ -413,6 +425,8 @@ mod tests { assert_eq!(ModelType::Kling21Master.to_str(), "kling_2p1_master"); assert_eq!(ModelType::Kling2p5TurboPro.to_str(), "kling_2p5_turbo_pro"); assert_eq!(ModelType::Kling2p6Pro.to_str(), "kling_2p6_pro"); + assert_eq!(ModelType::Kling3p0Standard.to_str(), "kling_3p0_standard"); + assert_eq!(ModelType::Kling3p0Pro.to_str(), "kling_3p0_pro"); assert_eq!(ModelType::Seedance10Lite.to_str(), "seedance_1p0_lite"); assert_eq!(ModelType::Seedance10Pro.to_str(), "seedance_1p0_pro"); assert_eq!(ModelType::Seedance1p5Pro.to_str(), "seedance_1p5_pro"); @@ -469,6 +483,8 @@ mod tests { assert_eq!(ModelType::from_str("kling_2p1_master").unwrap(), ModelType::Kling21Master); assert_eq!(ModelType::from_str("kling_2p5_turbo_pro").unwrap(), ModelType::Kling2p5TurboPro); assert_eq!(ModelType::from_str("kling_2p6_pro").unwrap(), ModelType::Kling2p6Pro); + assert_eq!(ModelType::from_str("kling_3p0_standard").unwrap(), ModelType::Kling3p0Standard); + assert_eq!(ModelType::from_str("kling_3p0_pro").unwrap(), ModelType::Kling3p0Pro); assert_eq!(ModelType::from_str("seedance_1p0_lite").unwrap(), ModelType::Seedance10Lite); assert_eq!(ModelType::from_str("seedance_1p0_pro").unwrap(), ModelType::Seedance10Pro); assert_eq!(ModelType::from_str("seedance_1p5_pro").unwrap(), ModelType::Seedance1p5Pro); @@ -490,7 +506,7 @@ mod tests { #[test] fn all_variants() { let mut variants = ModelType::all_variants(); - assert_eq!(variants.len(), 48); + assert_eq!(variants.len(), 50); // Image models assert_eq!(variants.pop_first(), Some(ModelType::Flux1Dev)); assert_eq!(variants.pop_first(), Some(ModelType::Flux1Schnell)); @@ -527,6 +543,8 @@ mod tests { assert_eq!(variants.pop_first(), Some(ModelType::Kling21Master)); assert_eq!(variants.pop_first(), Some(ModelType::Kling2p5TurboPro)); assert_eq!(variants.pop_first(), Some(ModelType::Kling2p6Pro)); + assert_eq!(variants.pop_first(), Some(ModelType::Kling3p0Standard)); + assert_eq!(variants.pop_first(), Some(ModelType::Kling3p0Pro)); assert_eq!(variants.pop_first(), Some(ModelType::Seedance10Lite)); assert_eq!(variants.pop_first(), Some(ModelType::Seedance10Pro)); assert_eq!(variants.pop_first(), Some(ModelType::Seedance1p5Pro)); diff --git a/crates/schema/public/enums/src/tauri/tasks/task_model_type.rs b/crates/schema/public/enums/src/tauri/tasks/task_model_type.rs index b2ae645b53..6545dbc7f0 100644 --- a/crates/schema/public/enums/src/tauri/tasks/task_model_type.rs +++ b/crates/schema/public/enums/src/tauri/tasks/task_model_type.rs @@ -67,6 +67,10 @@ pub enum TaskModelType { Kling2p5TurboPro, #[serde(rename = "kling_2p6_pro")] Kling2p6Pro, + #[serde(rename = "kling_3p0_standard")] + Kling3p0Standard, + #[serde(rename = "kling_3p0_pro")] + Kling3p0Pro, #[serde(rename = "seedance_1.0_lite")] Seedance10Lite, #[serde(rename = "seedance_1p5_pro")] @@ -135,6 +139,8 @@ impl TaskModelType { Self::Kling21Master => "kling_2.1_master", Self::Kling2p5TurboPro => "kling_2p5_turbo_pro", Self::Kling2p6Pro => "kling_2p6_pro", + Self::Kling3p0Standard => "kling_3p0_standard", + Self::Kling3p0Pro => "kling_3p0_pro", Self::Seedance10Lite => "seedance_1.0_lite", Self::Seedance1p5Pro => "seedance_1p5_pro", Self::Seedance2p0 => "seedance_2p0", @@ -182,6 +188,8 @@ impl TaskModelType { "kling_2.1_master" => Ok(Self::Kling21Master), "kling_2p5_turbo_pro" => Ok(Self::Kling2p5TurboPro), "kling_2p6_pro" => Ok(Self::Kling2p6Pro), + "kling_3p0_standard" => Ok(Self::Kling3p0Standard), + "kling_3p0_pro" => Ok(Self::Kling3p0Pro), "seedance_1.0_lite" => Ok(Self::Seedance10Lite), "seedance_1p5_pro" => Ok(Self::Seedance1p5Pro), "seedance_2p0" => Ok(Self::Seedance2p0), @@ -232,6 +240,8 @@ impl TaskModelType { Self::Kling21Master, Self::Kling2p5TurboPro, Self::Kling2p6Pro, + Self::Kling3p0Standard, + Self::Kling3p0Pro, Self::Seedance10Lite, Self::Seedance1p5Pro, Self::Seedance2p0, @@ -289,6 +299,8 @@ mod tests { assert_serialization(TaskModelType::Kling21Master, "kling_2.1_master"); assert_serialization(TaskModelType::Kling2p5TurboPro, "kling_2p5_turbo_pro"); assert_serialization(TaskModelType::Kling2p6Pro, "kling_2p6_pro"); + assert_serialization(TaskModelType::Kling3p0Standard, "kling_3p0_standard"); + assert_serialization(TaskModelType::Kling3p0Pro, "kling_3p0_pro"); assert_serialization(TaskModelType::Seedance10Lite, "seedance_1.0_lite"); assert_serialization(TaskModelType::Seedance1p5Pro, "seedance_1p5_pro"); assert_serialization(TaskModelType::Seedance2p0, "seedance_2p0"); @@ -335,6 +347,8 @@ mod tests { assert_eq!(TaskModelType::Kling21Master.to_str(), "kling_2.1_master"); assert_eq!(TaskModelType::Kling2p5TurboPro.to_str(), "kling_2p5_turbo_pro"); assert_eq!(TaskModelType::Kling2p6Pro.to_str(), "kling_2p6_pro"); + assert_eq!(TaskModelType::Kling3p0Standard.to_str(), "kling_3p0_standard"); + assert_eq!(TaskModelType::Kling3p0Pro.to_str(), "kling_3p0_pro"); assert_eq!(TaskModelType::Seedance10Lite.to_str(), "seedance_1.0_lite"); assert_eq!(TaskModelType::Seedance1p5Pro.to_str(), "seedance_1p5_pro"); assert_eq!(TaskModelType::Seedance2p0.to_str(), "seedance_2p0"); @@ -381,6 +395,8 @@ mod tests { assert_eq!(TaskModelType::from_str("kling_2.1_master").unwrap(), TaskModelType::Kling21Master); assert_eq!(TaskModelType::from_str("kling_2p5_turbo_pro").unwrap(), TaskModelType::Kling2p5TurboPro); assert_eq!(TaskModelType::from_str("kling_2p6_pro").unwrap(), TaskModelType::Kling2p6Pro); + assert_eq!(TaskModelType::from_str("kling_3p0_standard").unwrap(), TaskModelType::Kling3p0Standard); + assert_eq!(TaskModelType::from_str("kling_3p0_pro").unwrap(), TaskModelType::Kling3p0Pro); assert_eq!(TaskModelType::from_str("seedance_1.0_lite").unwrap(), TaskModelType::Seedance10Lite); assert_eq!(TaskModelType::from_str("seedance_1p5_pro").unwrap(), TaskModelType::Seedance1p5Pro); assert_eq!(TaskModelType::from_str("seedance_2p0").unwrap(), TaskModelType::Seedance2p0); @@ -412,7 +428,7 @@ mod tests { #[test] fn all_variants() { let mut variants = TaskModelType::all_variants(); - assert_eq!(variants.len(), 39); + assert_eq!(variants.len(), 41); // Image models assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Dev)); assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Schnell)); @@ -440,6 +456,8 @@ mod tests { assert_eq!(variants.pop_first(), Some(TaskModelType::Kling21Master)); assert_eq!(variants.pop_first(), Some(TaskModelType::Kling2p5TurboPro)); assert_eq!(variants.pop_first(), Some(TaskModelType::Kling2p6Pro)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Kling3p0Standard)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Kling3p0Pro)); assert_eq!(variants.pop_first(), Some(TaskModelType::Seedance10Lite)); assert_eq!(variants.pop_first(), Some(TaskModelType::Seedance1p5Pro)); assert_eq!(variants.pop_first(), Some(TaskModelType::Seedance2p0)); diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index 895ff37fcf..664b287951 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -251,6 +251,8 @@ use artcraft_api_defs::generate::video::generate_veo_3_fast_image_to_video::*; // Video multi-function use artcraft_api_defs::generate::video::multi_function::kling_2_5_turbo_multi_function_video_gen::*; use artcraft_api_defs::generate::video::multi_function::kling_2_6_multi_function_video_gen::*; +use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::*; +use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::*; use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::*; use artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::*; use artcraft_api_defs::generate::video::multi_function::sora_2_multi_function_video_gen::*; @@ -427,6 +429,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux // Generate Video (Multi-Function) crate::http_server::endpoints::generate::video::multi_function::kling_2p5_turbo_pro_multi_function_video_gen_handler::kling_2p5_turbo_pro_multi_function_video_gen_handler, crate::http_server::endpoints::generate::video::multi_function::kling_2p6_pro_multi_function_video_gen_handler::kling_2p6_pro_multi_function_video_gen_handler, + crate::http_server::endpoints::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen_handler::kling_3p0_pro_multi_function_video_gen_handler, + crate::http_server::endpoints::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen_handler::kling_3p0_standard_multi_function_video_gen_handler, crate::http_server::endpoints::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen_handler::seedance_1p5_pro_multi_function_video_gen_handler, crate::http_server::endpoints::generate::video::multi_function::seedance_2p0_multi_function_video_gen_handler::seedance_2p0_multi_function_video_gen_handler, crate::http_server::endpoints::generate::video::multi_function::sora_2_multi_function_video_gen_handler::sora_2_multi_function_video_gen_handler, @@ -1025,6 +1029,14 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux Kling2p5TurboProMultiFunctionVideoGenResponse, Kling2p6ProMultiFunctionVideoGenRequest, Kling2p6ProMultiFunctionVideoGenResponse, + Kling3p0ProMultiFunctionVideoGenRequest, + Kling3p0ProMultiFunctionVideoGenResponse, + Kling3p0ProMultiFunctionVideoGenDuration, + Kling3p0ProMultiFunctionVideoGenAspectRatio, + Kling3p0StandardMultiFunctionVideoGenRequest, + Kling3p0StandardMultiFunctionVideoGenResponse, + Kling3p0StandardMultiFunctionVideoGenDuration, + Kling3p0StandardMultiFunctionVideoGenAspectRatio, Seedance2p0MultiFunctionVideoGenRequest, Seedance2p0MultiFunctionVideoGenResponse, Sora2MultiFunctionVideoGenRequest, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/video/estimate_video_cost_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/video/estimate_video_cost_handler.rs index 24c1dd03f9..0e227ad4ac 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/video/estimate_video_cost_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/video/estimate_video_cost_handler.rs @@ -130,6 +130,8 @@ fn map_video_model(model: CommonVideoModel) -> Result RouterVideoModel::Kling21Master, CommonVideoModel::Kling2p5TurboPro => RouterVideoModel::Kling2p5TurboPro, CommonVideoModel::Kling2p6Pro => RouterVideoModel::Kling2p6Pro, + CommonVideoModel::Kling3p0Standard => RouterVideoModel::Kling3p0Standard, + CommonVideoModel::Kling3p0Pro => RouterVideoModel::Kling3p0Pro, CommonVideoModel::Seedance10Lite => RouterVideoModel::Seedance10Lite, CommonVideoModel::Seedance1p5Pro => RouterVideoModel::Seedance1p5Pro, CommonVideoModel::Seedance2p0 => RouterVideoModel::Seedance2p0, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/kling_3p0_pro_multi_function_video_gen_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/kling_3p0_pro_multi_function_video_gen_handler.rs new file mode 100644 index 0000000000..d639c3523c --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/kling_3p0_pro_multi_function_video_gen_handler.rs @@ -0,0 +1,377 @@ +use std::collections::{HashMap, HashSet}; +use std::iter::FromIterator; +use std::sync::Arc; + +use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder; +use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::state::server_state::ServerState; +use crate::util::lookup::lookup_image_urls_as_map::lookup_image_urls_as_map; +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; +use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::{Kling3p0ProMultiFunctionVideoGenAspectRatio, Kling3p0ProMultiFunctionVideoGenDuration, Kling3p0ProMultiFunctionVideoGenRequest, Kling3p0ProMultiFunctionVideoGenResponse}; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType; +use enums::by_table::prompts::prompt_type::PromptType; +use enums::common::generation_provider::GenerationProvider; +use enums::common::model_type::ModelType; +use enums::common::visibility::Visibility; +use fal_client::creds::open_ai_api_key::OpenAiApiKey; +use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator; +use fal_client::requests::webhook::video::image::enqueue_kling_v3_pro_image_to_video_webhook::{enqueue_kling_v3_pro_image_to_video_webhook, EnqueueKlingV3ProImageToVideoArgs}; +use fal_client::requests::webhook::video::text::enqueue_kling_v3_pro_text_to_video_webhook::{enqueue_kling_v3_pro_text_to_video_webhook, EnqueueKlingV3ProTextToVideoArgs, EnqueueKlingV3ProTextToVideoAspectRatio, EnqueueKlingV3ProTextToVideoDuration}; +use http_server_common::request::get_request_ip::get_request_ip; +use log::{error, info, warn}; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_with_apriori_job_token::{insert_generic_inference_job_for_fal_queue_with_apriori_job_token, InsertGenericInferenceForFalWithAprioriJobTokenArgs}; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection}; +use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem}; +use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs}; +use mysql_queries::queries::wallets::spend::wallet_spend_error::WalletSpendError; +use server_environment::ServerEnvironment; +use sqlx::pool::PoolConnection; +use sqlx::{Acquire, MySql}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +fn map_duration_i2v(duration: Option) -> EnqueueKlingV3ProTextToVideoDuration { + match duration { + Some(Kling3p0ProMultiFunctionVideoGenDuration::ThreeSeconds) => EnqueueKlingV3ProTextToVideoDuration::ThreeSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::FourSeconds) => EnqueueKlingV3ProTextToVideoDuration::FourSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::FiveSeconds) => EnqueueKlingV3ProTextToVideoDuration::FiveSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::SixSeconds) => EnqueueKlingV3ProTextToVideoDuration::SixSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::SevenSeconds) => EnqueueKlingV3ProTextToVideoDuration::SevenSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::EightSeconds) => EnqueueKlingV3ProTextToVideoDuration::EightSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::NineSeconds) => EnqueueKlingV3ProTextToVideoDuration::NineSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::TenSeconds) => EnqueueKlingV3ProTextToVideoDuration::TenSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::ElevenSeconds) => EnqueueKlingV3ProTextToVideoDuration::ElevenSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::TwelveSeconds) => EnqueueKlingV3ProTextToVideoDuration::TwelveSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::ThirteenSeconds) => EnqueueKlingV3ProTextToVideoDuration::ThirteenSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::FourteenSeconds) => EnqueueKlingV3ProTextToVideoDuration::FourteenSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::FifteenSeconds) => EnqueueKlingV3ProTextToVideoDuration::FifteenSeconds, + None => EnqueueKlingV3ProTextToVideoDuration::FiveSeconds, + } +} + +fn map_duration_t2v(duration: Option) -> EnqueueKlingV3ProTextToVideoDuration { + match duration { + Some(Kling3p0ProMultiFunctionVideoGenDuration::ThreeSeconds) => EnqueueKlingV3ProTextToVideoDuration::ThreeSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::FourSeconds) => EnqueueKlingV3ProTextToVideoDuration::FourSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::FiveSeconds) => EnqueueKlingV3ProTextToVideoDuration::FiveSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::SixSeconds) => EnqueueKlingV3ProTextToVideoDuration::SixSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::SevenSeconds) => EnqueueKlingV3ProTextToVideoDuration::SevenSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::EightSeconds) => EnqueueKlingV3ProTextToVideoDuration::EightSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::NineSeconds) => EnqueueKlingV3ProTextToVideoDuration::NineSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::TenSeconds) => EnqueueKlingV3ProTextToVideoDuration::TenSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::ElevenSeconds) => EnqueueKlingV3ProTextToVideoDuration::ElevenSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::TwelveSeconds) => EnqueueKlingV3ProTextToVideoDuration::TwelveSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::ThirteenSeconds) => EnqueueKlingV3ProTextToVideoDuration::ThirteenSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::FourteenSeconds) => EnqueueKlingV3ProTextToVideoDuration::FourteenSeconds, + Some(Kling3p0ProMultiFunctionVideoGenDuration::FifteenSeconds) => EnqueueKlingV3ProTextToVideoDuration::FifteenSeconds, + None => EnqueueKlingV3ProTextToVideoDuration::FiveSeconds, + } +} + +fn map_aspect_ratio_t2v(aspect_ratio: Option) -> EnqueueKlingV3ProTextToVideoAspectRatio { + match aspect_ratio { + Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::Square) => EnqueueKlingV3ProTextToVideoAspectRatio::Square, + Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::SixteenByNine) => EnqueueKlingV3ProTextToVideoAspectRatio::SixteenByNine, + Some(Kling3p0ProMultiFunctionVideoGenAspectRatio::NineBySixteen) => EnqueueKlingV3ProTextToVideoAspectRatio::NineBySixteen, + None => EnqueueKlingV3ProTextToVideoAspectRatio::Square, + } +} + +/// Kling 3.0 Pro Multi-Function (text and image to video) +#[utoipa::path( + post, + tag = "Generate Video (Multi-Function)", + path = "/v1/generate/video/multi_function/kling_3p0_pro", + responses( + (status = 200, description = "Success", body = Kling3p0ProMultiFunctionVideoGenResponse), + ), + params( + ("request" = Kling3p0ProMultiFunctionVideoGenRequest, description = "Payload for Request"), + ) +)] +pub async fn kling_3p0_pro_multi_function_video_gen_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + + payments_error_test(&request.prompt.as_deref().unwrap_or(""))?; + + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await?; + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + let user_token = match maybe_user_session.as_ref() { + Some(session) => &session.user_token, + None => { + return Err(CommonWebError::NotAuthorized); + } + }; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + let mut query_media_tokens = None; + + if let Some(start_frame_token) = request.image_media_token.as_ref() { + let mut tokens = Vec::new(); + tokens.push(start_frame_token.to_owned()); + + if let Some(end_frame_token) = request.end_image_media_token.as_ref() { + tokens.push(end_frame_token.to_owned()); + } + + query_media_tokens = Some(tokens); + } + + let image_urls_by_token = match query_media_tokens { + None => HashMap::new(), + Some(media_tokens) => { + info!("Looking up image media tokens: {:?}", media_tokens); + lookup_image_urls_as_map( + &http_request, + &mut mysql_connection, + server_state.server_environment, + &media_tokens, + ).await? + } + }; + + let maybe_start_frame_image_url = match request.image_media_token.as_ref() { + None => None, + Some(token) => match image_urls_by_token.get(token) { + Some(url) => Some(url.to_string()), + None => { + return Err(CommonWebError::BadInputWithSimpleMessage("Media for start frame not found.".to_string())); + } + } + }; + + let maybe_end_frame_image_url = match request.end_image_media_token.as_ref() { + None => None, + Some(token) => match image_urls_by_token.get(token) { + Some(url) => Some(url.to_string()), + None => { + return Err(CommonWebError::BadInputWithSimpleMessage("Media for end frame not found.".to_string())); + } + } + }; + + insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string()) + })?; + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let apriori_job_token = InferenceJobToken::generate(); + + let fal_result; + + if let Some(start_frame_url) = maybe_start_frame_image_url { + info!("image-to-video case"); + + let duration = map_duration_i2v(request.duration); + + let args = EnqueueKlingV3ProImageToVideoArgs { + 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, + negative_prompt: request.negative_prompt.clone(), + duration: Some(duration), + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + let cost = args.calculate_cost_in_cents(); + + info!("Charging wallet: {}", cost); + + attempt_wallet_deduction_else_common_web_error( + user_token, + Some(apriori_job_token.as_str()), + cost, + &mut mysql_connection, + ).await?; + + fal_result = enqueue_kling_v3_pro_image_to_video_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_kling_v3_pro_image_to_video_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + } else { + info!("text-to-video case"); + + let duration = map_duration_t2v(request.duration); + + let aspect_ratio = map_aspect_ratio_t2v(request.aspect_ratio); + + let args = EnqueueKlingV3ProTextToVideoArgs { + prompt: request.prompt.as_deref().unwrap_or("").to_string(), + negative_prompt: request.negative_prompt.clone(), + generate_audio: request.generate_audio, + duration: Some(duration), + aspect_ratio: Some(aspect_ratio), + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + let cost = args.calculate_cost_in_cents(); + + info!("Charging wallet..."); + + attempt_wallet_deduction_else_common_web_error( + user_token, + Some(apriori_job_token.as_str()), + cost, + &mut mysql_connection, + ).await?; + + fal_result = enqueue_kling_v3_pro_text_to_video_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_kling_v3_pro_text_to_video_webhook: {:?}", err); + CommonWebError::ServerError + })?; + } + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let mut transaction = mysql_connection + .begin() + .await + .map_err(|err| { + error!("Error starting MySQL transaction: {:?}", err); + CommonWebError::ServerError + })?; + + // NB: Don't fail the job if the query fails. + let prompt_result = insert_prompt(InsertPromptArgs { + maybe_apriori_prompt_token: None, + prompt_type: PromptType::ArtcraftApp, + maybe_creator_user_token: maybe_user_session + .as_ref() + .map(|s| &s.user_token), + maybe_model_type: Some(ModelType::Kling3p0Pro), + maybe_generation_provider: Some(GenerationProvider::Artcraft), + maybe_positive_prompt: request.prompt.as_deref(), + maybe_negative_prompt: None, + maybe_other_args: None, + creator_ip_address: &ip_address, + mysql_executor: &mut *transaction, + phantom: Default::default(), + }).await; + + let prompt_token = match prompt_result { + Ok(token) => Some(token), + Err(err) => { + warn!("Error inserting prompt: {:?}", err); + None // Don't fail the job if the prompt insertion fails. + } + }; + + if let Some(token) = prompt_token.as_ref() { + let mut context_items = Vec::with_capacity(2); + + if let Some(media_token) = &request.image_media_token { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::VidStartFrame, + }); + } + + if let Some(media_token) = &request.end_image_media_token { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::VidEndFrame, + }); + } + + if !context_items.is_empty() { + let result = insert_batch_prompt_context_items(InsertBatchArgs { + prompt_token: token.clone(), + items: context_items, + transaction: &mut transaction, + }).await; + + if let Err(err) = result { + // NB: Fail open. + warn!("Error inserting batch prompt context items: {:?}", err); + } + } + } + + let db_result = insert_generic_inference_job_for_fal_queue_with_apriori_job_token(InsertGenericInferenceForFalWithAprioriJobTokenArgs { + apriori_job_token: &apriori_job_token, + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::VideoGeneration, + maybe_inference_args: None, + maybe_prompt_token: prompt_token.as_ref(), + maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_executor: &mut *transaction, + starting_job_status_override: None, + phantom: Default::default(), + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + let _r = transaction + .commit() + .await + .map_err(|err| { + error!("Error committing MySQL transaction: {:?}", err); + CommonWebError::ServerError + })?; + + Ok(Json(Kling3p0ProMultiFunctionVideoGenResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/kling_3p0_standard_multi_function_video_gen_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/kling_3p0_standard_multi_function_video_gen_handler.rs new file mode 100644 index 0000000000..a2a96890b8 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/kling_3p0_standard_multi_function_video_gen_handler.rs @@ -0,0 +1,377 @@ +use std::collections::{HashMap, HashSet}; +use std::iter::FromIterator; +use std::sync::Arc; + +use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder; +use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use crate::state::server_state::ServerState; +use crate::util::lookup::lookup_image_urls_as_map::lookup_image_urls_as_map; +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; +use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::{Kling3p0StandardMultiFunctionVideoGenAspectRatio, Kling3p0StandardMultiFunctionVideoGenDuration, Kling3p0StandardMultiFunctionVideoGenRequest, Kling3p0StandardMultiFunctionVideoGenResponse}; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType; +use enums::by_table::prompts::prompt_type::PromptType; +use enums::common::generation_provider::GenerationProvider; +use enums::common::model_type::ModelType; +use enums::common::visibility::Visibility; +use fal_client::creds::open_ai_api_key::OpenAiApiKey; +use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator; +use fal_client::requests::webhook::video::image::enqueue_kling_v3_standard_image_to_video_webhook::{enqueue_kling_v3_standard_image_to_video_webhook, EnqueueKlingV3StandardImageToVideoArgs}; +use fal_client::requests::webhook::video::text::enqueue_kling_v3_standard_text_to_video_webhook::{enqueue_kling_v3_standard_text_to_video_webhook, EnqueueKlingV3StandardTextToVideoArgs, EnqueueKlingV3StandardTextToVideoAspectRatio, EnqueueKlingV3StandardTextToVideoDuration}; +use http_server_common::request::get_request_ip::get_request_ip; +use log::{error, info, warn}; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_with_apriori_job_token::{insert_generic_inference_job_for_fal_queue_with_apriori_job_token, InsertGenericInferenceForFalWithAprioriJobTokenArgs}; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection}; +use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem}; +use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs}; +use mysql_queries::queries::wallets::spend::wallet_spend_error::WalletSpendError; +use server_environment::ServerEnvironment; +use sqlx::pool::PoolConnection; +use sqlx::{Acquire, MySql}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +fn map_duration_i2v(duration: Option) -> EnqueueKlingV3StandardTextToVideoDuration { + match duration { + Some(Kling3p0StandardMultiFunctionVideoGenDuration::ThreeSeconds) => EnqueueKlingV3StandardTextToVideoDuration::ThreeSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::FourSeconds) => EnqueueKlingV3StandardTextToVideoDuration::FourSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::FiveSeconds) => EnqueueKlingV3StandardTextToVideoDuration::FiveSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::SixSeconds) => EnqueueKlingV3StandardTextToVideoDuration::SixSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::SevenSeconds) => EnqueueKlingV3StandardTextToVideoDuration::SevenSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::EightSeconds) => EnqueueKlingV3StandardTextToVideoDuration::EightSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::NineSeconds) => EnqueueKlingV3StandardTextToVideoDuration::NineSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::TenSeconds) => EnqueueKlingV3StandardTextToVideoDuration::TenSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::ElevenSeconds) => EnqueueKlingV3StandardTextToVideoDuration::ElevenSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::TwelveSeconds) => EnqueueKlingV3StandardTextToVideoDuration::TwelveSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::ThirteenSeconds) => EnqueueKlingV3StandardTextToVideoDuration::ThirteenSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::FourteenSeconds) => EnqueueKlingV3StandardTextToVideoDuration::FourteenSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::FifteenSeconds) => EnqueueKlingV3StandardTextToVideoDuration::FifteenSeconds, + None => EnqueueKlingV3StandardTextToVideoDuration::FiveSeconds, + } +} + +fn map_duration_t2v(duration: Option) -> EnqueueKlingV3StandardTextToVideoDuration { + match duration { + Some(Kling3p0StandardMultiFunctionVideoGenDuration::ThreeSeconds) => EnqueueKlingV3StandardTextToVideoDuration::ThreeSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::FourSeconds) => EnqueueKlingV3StandardTextToVideoDuration::FourSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::FiveSeconds) => EnqueueKlingV3StandardTextToVideoDuration::FiveSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::SixSeconds) => EnqueueKlingV3StandardTextToVideoDuration::SixSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::SevenSeconds) => EnqueueKlingV3StandardTextToVideoDuration::SevenSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::EightSeconds) => EnqueueKlingV3StandardTextToVideoDuration::EightSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::NineSeconds) => EnqueueKlingV3StandardTextToVideoDuration::NineSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::TenSeconds) => EnqueueKlingV3StandardTextToVideoDuration::TenSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::ElevenSeconds) => EnqueueKlingV3StandardTextToVideoDuration::ElevenSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::TwelveSeconds) => EnqueueKlingV3StandardTextToVideoDuration::TwelveSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::ThirteenSeconds) => EnqueueKlingV3StandardTextToVideoDuration::ThirteenSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::FourteenSeconds) => EnqueueKlingV3StandardTextToVideoDuration::FourteenSeconds, + Some(Kling3p0StandardMultiFunctionVideoGenDuration::FifteenSeconds) => EnqueueKlingV3StandardTextToVideoDuration::FifteenSeconds, + None => EnqueueKlingV3StandardTextToVideoDuration::FiveSeconds, + } +} + +fn map_aspect_ratio_t2v(aspect_ratio: Option) -> EnqueueKlingV3StandardTextToVideoAspectRatio { + match aspect_ratio { + Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::Square) => EnqueueKlingV3StandardTextToVideoAspectRatio::Square, + Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::SixteenByNine) => EnqueueKlingV3StandardTextToVideoAspectRatio::SixteenByNine, + Some(Kling3p0StandardMultiFunctionVideoGenAspectRatio::NineBySixteen) => EnqueueKlingV3StandardTextToVideoAspectRatio::NineBySixteen, + None => EnqueueKlingV3StandardTextToVideoAspectRatio::Square, + } +} + +/// Kling 3.0 Standard Multi-Function (text and image to video) +#[utoipa::path( + post, + tag = "Generate Video (Multi-Function)", + path = "/v1/generate/video/multi_function/kling_3p0_standard", + responses( + (status = 200, description = "Success", body = Kling3p0StandardMultiFunctionVideoGenResponse), + ), + params( + ("request" = Kling3p0StandardMultiFunctionVideoGenRequest, description = "Payload for Request"), + ) +)] +pub async fn kling_3p0_standard_multi_function_video_gen_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data> +) -> Result, CommonWebError> { + + payments_error_test(&request.prompt.as_deref().unwrap_or(""))?; + + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await?; + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + CommonWebError::ServerError + })?; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + let user_token = match maybe_user_session.as_ref() { + Some(session) => &session.user_token, + None => { + return Err(CommonWebError::NotAuthorized); + } + }; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + let mut query_media_tokens = None; + + if let Some(start_frame_token) = request.image_media_token.as_ref() { + let mut tokens = Vec::new(); + tokens.push(start_frame_token.to_owned()); + + if let Some(end_frame_token) = request.end_image_media_token.as_ref() { + tokens.push(end_frame_token.to_owned()); + } + + query_media_tokens = Some(tokens); + } + + let image_urls_by_token = match query_media_tokens { + None => HashMap::new(), + Some(media_tokens) => { + info!("Looking up image media tokens: {:?}", media_tokens); + lookup_image_urls_as_map( + &http_request, + &mut mysql_connection, + server_state.server_environment, + &media_tokens, + ).await? + } + }; + + let maybe_start_frame_image_url = match request.image_media_token.as_ref() { + None => None, + Some(token) => match image_urls_by_token.get(token) { + Some(url) => Some(url.to_string()), + None => { + return Err(CommonWebError::BadInputWithSimpleMessage("Media for start frame not found.".to_string())); + } + } + }; + + let maybe_end_frame_image_url = match request.end_image_media_token.as_ref() { + None => None, + Some(token) => match image_urls_by_token.get(token) { + Some(url) => Some(url.to_string()), + None => { + return Err(CommonWebError::BadInputWithSimpleMessage("Media for end frame not found.".to_string())); + } + } + }; + + insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string()) + })?; + + info!("Fal webhook URL: {}", server_state.fal.webhook_url); + + let apriori_job_token = InferenceJobToken::generate(); + + let fal_result; + + if let Some(start_frame_url) = maybe_start_frame_image_url { + info!("image-to-video case"); + + let duration = map_duration_i2v(request.duration); + + let args = EnqueueKlingV3StandardImageToVideoArgs { + 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, + negative_prompt: request.negative_prompt.clone(), + duration: Some(duration), + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + let cost = args.calculate_cost_in_cents(); + + info!("Charging wallet: {}", cost); + + attempt_wallet_deduction_else_common_web_error( + user_token, + Some(apriori_job_token.as_str()), + cost, + &mut mysql_connection, + ).await?; + + fal_result = enqueue_kling_v3_standard_image_to_video_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_kling_v3_standard_image_to_video_webhook: {:?}", err); + CommonWebError::ServerError + })?; + + } else { + info!("text-to-video case"); + + let duration = map_duration_t2v(request.duration); + + let aspect_ratio = map_aspect_ratio_t2v(request.aspect_ratio); + + let args = EnqueueKlingV3StandardTextToVideoArgs { + prompt: request.prompt.as_deref().unwrap_or("").to_string(), + negative_prompt: request.negative_prompt.clone(), + generate_audio: request.generate_audio, + duration: Some(duration), + aspect_ratio: Some(aspect_ratio), + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + let cost = args.calculate_cost_in_cents(); + + info!("Charging wallet..."); + + attempt_wallet_deduction_else_common_web_error( + user_token, + Some(apriori_job_token.as_str()), + cost, + &mut mysql_connection, + ).await?; + + fal_result = enqueue_kling_v3_standard_text_to_video_webhook(args) + .await + .map_err(|err| { + warn!("Error calling enqueue_kling_v3_standard_text_to_video_webhook: {:?}", err); + CommonWebError::ServerError + })?; + } + + let external_job_id = fal_result.request_id + .ok_or_else(|| { + warn!("Fal request_id is None"); + CommonWebError::ServerError + })?; + + info!("Fal request_id: {}", external_job_id); + + let ip_address = get_request_ip(&http_request); + + let mut transaction = mysql_connection + .begin() + .await + .map_err(|err| { + error!("Error starting MySQL transaction: {:?}", err); + CommonWebError::ServerError + })?; + + // NB: Don't fail the job if the query fails. + let prompt_result = insert_prompt(InsertPromptArgs { + maybe_apriori_prompt_token: None, + prompt_type: PromptType::ArtcraftApp, + maybe_creator_user_token: maybe_user_session + .as_ref() + .map(|s| &s.user_token), + maybe_model_type: Some(ModelType::Kling3p0Standard), + maybe_generation_provider: Some(GenerationProvider::Artcraft), + maybe_positive_prompt: request.prompt.as_deref(), + maybe_negative_prompt: None, + maybe_other_args: None, + creator_ip_address: &ip_address, + mysql_executor: &mut *transaction, + phantom: Default::default(), + }).await; + + let prompt_token = match prompt_result { + Ok(token) => Some(token), + Err(err) => { + warn!("Error inserting prompt: {:?}", err); + None // Don't fail the job if the prompt insertion fails. + } + }; + + if let Some(token) = prompt_token.as_ref() { + let mut context_items = Vec::with_capacity(2); + + if let Some(media_token) = &request.image_media_token { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::VidStartFrame, + }); + } + + if let Some(media_token) = &request.end_image_media_token { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::VidEndFrame, + }); + } + + if !context_items.is_empty() { + let result = insert_batch_prompt_context_items(InsertBatchArgs { + prompt_token: token.clone(), + items: context_items, + transaction: &mut transaction, + }).await; + + if let Err(err) = result { + // NB: Fail open. + warn!("Error inserting batch prompt context items: {:?}", err); + } + } + } + + let db_result = insert_generic_inference_job_for_fal_queue_with_apriori_job_token(InsertGenericInferenceForFalWithAprioriJobTokenArgs { + apriori_job_token: &apriori_job_token, + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::VideoGeneration, + maybe_inference_args: None, + maybe_prompt_token: prompt_token.as_ref(), + maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_executor: &mut *transaction, + starting_job_status_override: None, + phantom: Default::default(), + }).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting generic inference job for FAL queue: {:?}", err); + return Err(CommonWebError::ServerError); + } + }; + + let _r = transaction + .commit() + .await + .map_err(|err| { + error!("Error committing MySQL transaction: {:?}", err); + CommonWebError::ServerError + })?; + + Ok(Json(Kling3p0StandardMultiFunctionVideoGenResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/mod.rs index 2c56b04091..5d14f65adb 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/mod.rs @@ -1,5 +1,7 @@ pub mod kling_2p5_turbo_pro_multi_function_video_gen_handler; pub mod kling_2p6_pro_multi_function_video_gen_handler; +pub mod kling_3p0_pro_multi_function_video_gen_handler; +pub mod kling_3p0_standard_multi_function_video_gen_handler; pub mod seedance_1p5_pro_multi_function_video_gen_handler; pub mod seedance_2p0_multi_function_video_gen_handler; pub mod sora_2_multi_function_video_gen_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/generate_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/generate_routes.rs index 0058f40e60..a0522ddf04 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/generate_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/generate_routes.rs @@ -33,6 +33,8 @@ use crate::http_server::endpoints::generate::video::generate_veo_3_fast_image_to use crate::http_server::endpoints::generate::video::generate_veo_3_image_to_video_handler::generate_veo_3_image_to_video_handler; use crate::http_server::endpoints::generate::video::multi_function::kling_2p5_turbo_pro_multi_function_video_gen_handler::kling_2p5_turbo_pro_multi_function_video_gen_handler; use crate::http_server::endpoints::generate::video::multi_function::kling_2p6_pro_multi_function_video_gen_handler::kling_2p6_pro_multi_function_video_gen_handler; +use crate::http_server::endpoints::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen_handler::kling_3p0_pro_multi_function_video_gen_handler; +use crate::http_server::endpoints::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen_handler::kling_3p0_standard_multi_function_video_gen_handler; use crate::http_server::endpoints::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen_handler::seedance_1p5_pro_multi_function_video_gen_handler; use crate::http_server::endpoints::generate::video::multi_function::seedance_2p0_multi_function_video_gen_handler::seedance_2p0_multi_function_video_gen_handler; use crate::http_server::endpoints::generate::video::multi_function::sora_2_multi_function_video_gen_handler::sora_2_multi_function_video_gen_handler; @@ -164,6 +166,14 @@ where .route(web::post().to(kling_2p6_pro_multi_function_video_gen_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service(web::resource("/kling_3p0_standard") + .route(web::post().to(kling_3p0_standard_multi_function_video_gen_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + .service(web::resource("/kling_3p0_pro") + .route(web::post().to(kling_3p0_pro_multi_function_video_gen_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) .service(web::resource("/seedance_1p5_pro") .route(web::post().to(seedance_1p5_pro_multi_function_video_gen_handler)) .route(web::head().to(|| HttpResponse::Ok())) diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/mod.rs index df04d9cb04..200077dead 100644 --- a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/mod.rs +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/mod.rs @@ -82,3 +82,17 @@ pub mod v2_5; ))) )] pub mod v2_6; +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_kling-video" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_kling-video" + ))) +)] +pub mod v3; diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_pro_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_pro_image_to_video.rs new file mode 100644 index 0000000000..9c847418f6 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_pro_image_to_video.rs @@ -0,0 +1,53 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct KlingV3ProImageToVideoInput { + pub prompt: String, + + /// Starting frame image URL + pub image_url: String, + + /// Optional end frame image URL + #[serde(skip_serializing_if = "Option::is_none")] + pub end_image_url: Option, + + /// Aspect ratio + /// Possible enum values: "16:9", "9:16", "1:1" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Generate audio + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_audio: Option, + + /// Optional negative prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + /// Duration in seconds + /// Options: "3" through "15" + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// The CFG (Classifier Free Guidance) scale. + /// Default value: 0.5 + #[serde(skip_serializing_if = "Option::is_none")] + pub cfg_scale: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KlingV3ProImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn kling_v3_pro_image_to_video( + params: KlingV3ProImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/kling-video/v3/pro/image-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_pro_text_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_pro_text_to_video.rs new file mode 100644 index 0000000000..b22d1adebc --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_pro_text_to_video.rs @@ -0,0 +1,46 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct KlingV3ProTextToVideoInput { + pub prompt: String, + + /// Aspect ratio + /// Possible enum values: "16:9", "9:16", "1:1" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Generate audio + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_audio: Option, + + /// Optional negative prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + /// Duration in seconds + /// Options: "3" through "15" + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// The CFG (Classifier Free Guidance) scale. + /// Default value: 0.5 + #[serde(skip_serializing_if = "Option::is_none")] + pub cfg_scale: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KlingV3ProTextToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn kling_v3_pro_text_to_video( + params: KlingV3ProTextToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/kling-video/v3/pro/text-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_standard_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_standard_image_to_video.rs new file mode 100644 index 0000000000..e15513553f --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_standard_image_to_video.rs @@ -0,0 +1,53 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct KlingV3StandardImageToVideoInput { + pub prompt: String, + + /// Starting frame image URL + pub image_url: String, + + /// Optional end frame image URL + #[serde(skip_serializing_if = "Option::is_none")] + pub end_image_url: Option, + + /// Aspect ratio + /// Possible enum values: "16:9", "9:16", "1:1" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Generate audio + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_audio: Option, + + /// Optional negative prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + /// Duration in seconds + /// Options: "3" through "15" + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// The CFG (Classifier Free Guidance) scale. + /// Default value: 0.5 + #[serde(skip_serializing_if = "Option::is_none")] + pub cfg_scale: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KlingV3StandardImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn kling_v3_standard_image_to_video( + params: KlingV3StandardImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/kling-video/v3/standard/image-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_standard_text_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_standard_text_to_video.rs new file mode 100644 index 0000000000..9e76d835f3 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/kling_v3_standard_text_to_video.rs @@ -0,0 +1,46 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct KlingV3StandardTextToVideoInput { + pub prompt: String, + + /// Aspect ratio + /// Possible enum values: "16:9", "9:16", "1:1" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Generate audio + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_audio: Option, + + /// Optional negative prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + /// Duration in seconds + /// Options: "3" through "15" + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// The CFG (Classifier Free Guidance) scale. + /// Default value: 0.5 + #[serde(skip_serializing_if = "Option::is_none")] + pub cfg_scale: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KlingV3StandardTextToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn kling_v3_standard_text_to_video( + params: KlingV3StandardTextToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/kling-video/v3/standard/text-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/mod.rs new file mode 100644 index 0000000000..d530b2415d --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v3/mod.rs @@ -0,0 +1,4 @@ +pub mod kling_v3_pro_image_to_video; +pub mod kling_v3_pro_text_to_video; +pub mod kling_v3_standard_image_to_video; +pub mod kling_v3_standard_text_to_video; diff --git a/frontend/apps/artcraft-website/tsconfig.app.json b/frontend/apps/artcraft-website/tsconfig.app.json index 00a86d267e..d32f0aa659 100644 --- a/frontend/apps/artcraft-website/tsconfig.app.json +++ b/frontend/apps/artcraft-website/tsconfig.app.json @@ -38,10 +38,7 @@ ], "references": [ { - "path": "../../libs/components/tab-selector/tsconfig.lib.json" - }, - { - "path": "../../libs/state/subscription/tsconfig.lib.json" + "path": "../../libs/components/viewer-3d/tsconfig.lib.json" }, { "path": "../../libs/model-list/tsconfig.lib.json" @@ -58,14 +55,20 @@ { "path": "../../libs/components/loading-spinner/tsconfig.lib.json" }, + { + "path": "../../libs/components/tab-selector/tsconfig.lib.json" + }, + { + "path": "../../libs/state/subscription/tsconfig.lib.json" + }, { "path": "../../libs/markdown-content/tsconfig.lib.json" }, { - "path": "../../libs/api/tsconfig.lib.json" + "path": "../../libs/components/button/tsconfig.lib.json" }, { - "path": "../../libs/components/button/tsconfig.lib.json" + "path": "../../libs/api/tsconfig.lib.json" } ] } diff --git a/frontend/apps/artcraft-website/tsconfig.json b/frontend/apps/artcraft-website/tsconfig.json index 9b1b3cdf03..40b378e25e 100644 --- a/frontend/apps/artcraft-website/tsconfig.json +++ b/frontend/apps/artcraft-website/tsconfig.json @@ -3,10 +3,7 @@ "include": [], "references": [ { - "path": "../../libs/components/tab-selector" - }, - { - "path": "../../libs/state/subscription" + "path": "../../libs/components/viewer-3d" }, { "path": "../../libs/model-list" @@ -23,14 +20,20 @@ { "path": "../../libs/components/loading-spinner" }, + { + "path": "../../libs/components/tab-selector" + }, + { + "path": "../../libs/state/subscription" + }, { "path": "../../libs/markdown-content" }, { - "path": "../../libs/api" + "path": "../../libs/components/button" }, { - "path": "../../libs/components/button" + "path": "../../libs/api" }, { "path": "./tsconfig.app.json" diff --git a/frontend/libs/api-enums/src/lib/common/generation/CommonVideoModel.ts b/frontend/libs/api-enums/src/lib/common/generation/CommonVideoModel.ts index 677b90ffca..f45f3de5f8 100644 --- a/frontend/libs/api-enums/src/lib/common/generation/CommonVideoModel.ts +++ b/frontend/libs/api-enums/src/lib/common/generation/CommonVideoModel.ts @@ -9,6 +9,8 @@ export enum CommonVideoModel { Kling21Master = "kling_2p1_master", Kling2p5TurboPro = "kling_2p5_turbo_pro", Kling2p6Pro = "kling_2p6_pro", + Kling3p0Standard = "kling_3p0_standard", + Kling3p0Pro = "kling_3p0_pro", Seedance10Lite = "seedance_1p0_lite", Seedance1p5Pro = "seedance_1p5_pro", Seedance2p0 = "seedance_2p0", diff --git a/frontend/libs/components/pricing-modal/src/lib/convert/videoModelToCommonVideoModel.ts b/frontend/libs/components/pricing-modal/src/lib/convert/videoModelToCommonVideoModel.ts index 9b691be491..38e5c6360b 100644 --- a/frontend/libs/components/pricing-modal/src/lib/convert/videoModelToCommonVideoModel.ts +++ b/frontend/libs/components/pricing-modal/src/lib/convert/videoModelToCommonVideoModel.ts @@ -18,8 +18,14 @@ export function videoModelToCommonVideoModel( return CommonVideoModel.Kling2p5TurboPro; case "kling_2p6_pro": return CommonVideoModel.Kling2p6Pro; + case "kling_3p0_standard": + return CommonVideoModel.Kling3p0Standard; + case "kling_3p0_pro": + return CommonVideoModel.Kling3p0Pro; case "seedance_1.0_lite": return CommonVideoModel.Seedance10Lite; + case "seedance_1p5_pro": + return CommonVideoModel.Seedance1p5Pro; case "seedance_2p0": return CommonVideoModel.Seedance2p0; case "sora_2": diff --git a/frontend/libs/components/pricing-modal/src/lib/cost-breakdown-modal.tsx b/frontend/libs/components/pricing-modal/src/lib/cost-breakdown-modal.tsx index a63da64af9..03ea872dc2 100644 --- a/frontend/libs/components/pricing-modal/src/lib/cost-breakdown-modal.tsx +++ b/frontend/libs/components/pricing-modal/src/lib/cost-breakdown-modal.tsx @@ -248,6 +248,9 @@ export function CostBreakdownModal({ activeTabId }: CostBreakdownModalProps) { "seedream_4", "seedream_4p5", "seedream_5_lite", + "kling_3p0_standard", + "kling_3p0_pro", + "seedance_1p5_pro", "seedance_2p0", ]); const hasCostData = diff --git a/frontend/libs/model-list/src/lib/lists/VideoModels.ts b/frontend/libs/model-list/src/lib/lists/VideoModels.ts index bf3e800933..4946c5160f 100644 --- a/frontend/libs/model-list/src/lib/lists/VideoModels.ts +++ b/frontend/libs/model-list/src/lib/lists/VideoModels.ts @@ -111,6 +111,74 @@ export const VIDEO_MODELS: VideoModel[] = [ progressBarTime: 100000, generateWithSound: false, }), + new VideoModel({ + id: "kling_3p0_standard", + tauriId: "kling_3p0_standard", + fullName: "Kling 3.0 Standard", + category: "video", + creator: ModelCreator.Kling, + selectorName: "Kling 3.0 Standard", + selectorDescription: "Standard quality video model", + selectorBadges: ["2 min."], + startFrame: true, + endFrame: true, + requiresImage: false, + sizeOptions: [ + { + tauriValue: "wide_sixteen_by_nine", + textLabel: "16:9", + icon: SizeIconOption.Landscape16x9, + }, + { + tauriValue: "square", + textLabel: "1:1", + icon: SizeIconOption.Square, + }, + { + tauriValue: "tall_nine_by_sixteen", + textLabel: "9:16", + icon: SizeIconOption.Portrait9x16, + }, + ], + durationOptions: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + defaultDuration: 5, + progressBarTime: 100000, + generateWithSound: false, + }), + new VideoModel({ + id: "kling_3p0_pro", + tauriId: "kling_3p0_pro", + fullName: "Kling 3.0 Pro", + category: "video", + creator: ModelCreator.Kling, + selectorName: "Kling 3.0 Pro", + selectorDescription: "High quality video model", + selectorBadges: ["2 min."], + startFrame: true, + endFrame: true, + requiresImage: false, + sizeOptions: [ + { + tauriValue: "wide_sixteen_by_nine", + textLabel: "16:9", + icon: SizeIconOption.Landscape16x9, + }, + { + tauriValue: "square", + textLabel: "1:1", + icon: SizeIconOption.Square, + }, + { + tauriValue: "tall_nine_by_sixteen", + textLabel: "9:16", + icon: SizeIconOption.Portrait9x16, + }, + ], + durationOptions: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + defaultDuration: 5, + progressBarTime: 100000, + generateWithSound: false, + }), new VideoModel({ id: "seedance_1_0_lite", tauriId: "seedance_1.0_lite",