From 488ef32cc34da5e077119beb92afa2c35e85fa92 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 8 May 2026 13:22:36 -0400 Subject: [PATCH] new endpoint --- .../video/edit/beeble_switchx_edit_video.rs | 20 ++ .../src/generate/video/edit/mod.rs | 1 + .../src/generate/video/mod.rs | 1 + .../video/edit/beeble_switchx_edit_video.rs | 20 ++ .../src/endpoints/generate/video/edit/mod.rs | 1 + .../src/endpoints/generate/video/mod.rs | 1 + .../src/requests/create_upload_url/mod.rs | 1 + .../create_upload_url/upload_bytes.rs | 50 +++ ...for_beeble_queue_with_apriori_job_token.rs | 168 ++++++++++ .../queries/generic_inference/beeble/mod.rs | 1 + .../src/queries/generic_inference/mod.rs | 1 + .../inference_job_external_third_party.rs | 12 +- .../inference_job_product_category.rs | 13 +- .../inference_job_type.rs | 12 +- .../common/generation/common_model_type.rs | 14 +- crates/service/web/storyteller_web/Cargo.toml | 1 + .../web/storyteller_web/src/docs/api_doc.rs | 4 + .../beeble_switchx_edit_video_gen_handler.rs | 309 ++++++++++++++++++ .../endpoints/generate/video/edit/mod.rs | 1 + .../endpoints/generate/video/mod.rs | 1 + .../application_routes/generate_routes.rs | 7 + .../service/web/storyteller_web/src/main.rs | 7 +- .../storyteller_web/src/state/server_state.rs | 9 + 23 files changed, 649 insertions(+), 6 deletions(-) create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/edit/beeble_switchx_edit_video.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/edit/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/edit/beeble_switchx_edit_video.rs create mode 100644 crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/edit/mod.rs create mode 100644 crates/api_clients/beeble_client/src/requests/create_upload_url/upload_bytes.rs create mode 100644 crates/schema/database/mysql_queries/src/queries/generic_inference/beeble/insert_generic_inference_job_for_beeble_queue_with_apriori_job_token.rs create mode 100644 crates/schema/database/mysql_queries/src/queries/generic_inference/beeble/mod.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/edit/beeble_switchx_edit_video_gen_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/edit/mod.rs diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/edit/beeble_switchx_edit_video.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/edit/beeble_switchx_edit_video.rs new file mode 100644 index 0000000000..63c81fe119 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/edit/beeble_switchx_edit_video.rs @@ -0,0 +1,20 @@ +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const BEEBLE_SWITCHX_EDIT_VIDEO_PATH: &str = "/v1/generate/video/edit/beeble_switchx"; + +#[derive(Serialize, Deserialize, ToSchema, Debug)] +pub struct BeebleSwitchXEditVideoRequest { + pub uuid_idempotency_token: String, + pub source_video_media_token: Option, + pub reference_image_media_token: Option, + pub prompt: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct BeebleSwitchXEditVideoResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/edit/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/edit/mod.rs new file mode 100644 index 0000000000..ad8f1b29f4 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/edit/mod.rs @@ -0,0 +1 @@ +pub mod beeble_switchx_edit_video; diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/mod.rs index dcbdb40fbb..a1a46f0e8b 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/mod.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/mod.rs @@ -1,3 +1,4 @@ +pub mod edit; pub mod generate_kling_1_6_pro_image_to_video; pub mod generate_kling_2_1_master_image_to_video; pub mod generate_kling_2_1_pro_image_to_video; diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/edit/beeble_switchx_edit_video.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/edit/beeble_switchx_edit_video.rs new file mode 100644 index 0000000000..d4ccaf0db8 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/edit/beeble_switchx_edit_video.rs @@ -0,0 +1,20 @@ +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::edit::beeble_switchx_edit_video::{ + BeebleSwitchXEditVideoRequest, BeebleSwitchXEditVideoResponse, BEEBLE_SWITCHX_EDIT_VIDEO_PATH, +}; + +pub async fn beeble_switchx_edit_video( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + request: BeebleSwitchXEditVideoRequest, +) -> Result { + Ok(basic_json_post_request( + api_host, + BEEBLE_SWITCHX_EDIT_VIDEO_PATH, + maybe_creds, + request, + ).await?) +} diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/edit/mod.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/edit/mod.rs new file mode 100644 index 0000000000..ad8f1b29f4 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/edit/mod.rs @@ -0,0 +1 @@ +pub mod beeble_switchx_edit_video; diff --git a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/mod.rs b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/mod.rs index 28a28bdbb2..ebae4b56f8 100644 --- a/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/mod.rs +++ b/crates/api_clients/artcraft/artcraft_client/src/endpoints/generate/video/mod.rs @@ -1,3 +1,4 @@ +pub mod edit; pub mod generate_kling_16_pro_image_to_video; pub mod generate_kling_21_master_image_to_video; pub mod generate_kling_21_pro_image_to_video; diff --git a/crates/api_clients/beeble_client/src/requests/create_upload_url/mod.rs b/crates/api_clients/beeble_client/src/requests/create_upload_url/mod.rs index 44f8b9de30..f79471678d 100644 --- a/crates/api_clients/beeble_client/src/requests/create_upload_url/mod.rs +++ b/crates/api_clients/beeble_client/src/requests/create_upload_url/mod.rs @@ -1,2 +1,3 @@ pub mod create_upload_url; mod request_types; +pub mod upload_bytes; diff --git a/crates/api_clients/beeble_client/src/requests/create_upload_url/upload_bytes.rs b/crates/api_clients/beeble_client/src/requests/create_upload_url/upload_bytes.rs new file mode 100644 index 0000000000..921cb1b62c --- /dev/null +++ b/crates/api_clients/beeble_client/src/requests/create_upload_url/upload_bytes.rs @@ -0,0 +1,50 @@ +use log::info; + +use crate::creds::beeble_api_key::BeebleApiKey; +use crate::error::beeble_client_error::BeebleClientError; +use crate::error::beeble_error::BeebleError; +use crate::error::beeble_generic_api_error::BeebleGenericApiError; +use crate::requests::create_upload_url::create_upload_url::{ + create_upload_url, CreateUploadUrlArgs, CreateUploadUrlSuccess, +}; + +/// Upload bytes to Beeble in one call (create presigned URL + PUT bytes). +/// +/// Returns the upload success with the `beeble_uri` to use in generation calls. +pub async fn upload_bytes_to_beeble( + api_key: &BeebleApiKey, + filename: &str, + content_type: &str, + bytes: Vec, +) -> Result { + info!("Uploading {} bytes to Beeble as {}", bytes.len(), filename); + + // 1. Get presigned URL. + let upload = create_upload_url(CreateUploadUrlArgs { + api_key: api_key.clone(), + filename: filename.to_string(), + }).await?; + + info!("Beeble upload URL obtained: {}", upload.beeble_uri); + + // 2. PUT bytes to presigned URL. + let client = reqwest::Client::new(); + let put_response = client.put(&upload.upload_url) + .header("Content-Type", content_type) + .body(bytes) + .send() + .await + .map_err(|err| BeebleGenericApiError::ReqwestError(err))?; + + let status = put_response.status(); + if !status.is_success() { + let body = put_response.text().await.unwrap_or_default(); + return Err(BeebleGenericApiError::UncategorizedBadResponseWithStatusAndBody { + status_code: status, + body, + }.into()); + } + + info!("Beeble upload PUT succeeded: {}", upload.beeble_uri); + Ok(upload) +} diff --git a/crates/schema/database/mysql_queries/src/queries/generic_inference/beeble/insert_generic_inference_job_for_beeble_queue_with_apriori_job_token.rs b/crates/schema/database/mysql_queries/src/queries/generic_inference/beeble/insert_generic_inference_job_for_beeble_queue_with_apriori_job_token.rs new file mode 100644 index 0000000000..affb476a50 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/generic_inference/beeble/insert_generic_inference_job_for_beeble_queue_with_apriori_job_token.rs @@ -0,0 +1,168 @@ +use anyhow::anyhow; +use log::info; +use sqlx::{Executor, MySql}; +use std::marker::PhantomData; + +use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory; +use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory; +use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty; +use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory; +use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType; +use enums::common::job_status_plus::JobStatusPlus; +use enums::common::visibility::Visibility; +use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken; +use tokens::tokens::prompts::PromptToken; +use tokens::tokens::users::UserToken; + +use crate::errors::database_query_error::DatabaseQueryError; +use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs; + + +pub struct InsertGenericInferenceForBeebleWithAprioriJobTokenArgs<'e, 'c, E> + where E: 'e + Executor<'c, Database = MySql> +{ + pub uuid_idempotency_token: &'e str, + + // NOTE: We'll generate this ahead of time so we can save it with billing info! + pub apriori_job_token: &'e InferenceJobToken, + + /// The external primary key identifier for the job. + pub maybe_external_third_party_id: &'e str, + + pub maybe_inference_args: Option, + + pub maybe_prompt_token: Option<&'e PromptToken>, + + pub maybe_creator_user_token: Option<&'e UserToken>, + pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>, + pub creator_ip_address: &'e str, + pub creator_set_visibility: Visibility, + + pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>, + + /// Override the initial job status. Defaults to `Pending` when `None`. + /// Set to `Some(JobStatusPlus::CompleteFailure)` for mock/test failure jobs. + pub starting_job_status_override: Option, + + pub maybe_frontend_failure_category: Option, + pub maybe_failure_reason: Option<&'e str>, + + pub mysql_executor: E, + + // TODO: Not sure if this works to tell the compiler we need the lifetime annotation. + // See: https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-lifetime-parameters + pub phantom: PhantomData<&'c E>, +} + +pub async fn insert_generic_inference_job_for_beeble_queue_with_apriori_job_token<'e, 'c : 'e, E>( + args: InsertGenericInferenceForBeebleWithAprioriJobTokenArgs<'e, 'c, E> +) -> Result + where E: 'e + Executor<'c, Database = MySql> +{ + let serialized_args_payload = serde_json::ser::to_string(&args.maybe_inference_args) + .map_err(|_e| anyhow!("could not encode inference args"))?; + + const JOB_TYPE: InferenceJobType = InferenceJobType::BeebleQueue; + const INFERENCE_CATEGORY: InferenceCategory = InferenceCategory::VideoGeneration; + const PRODUCT_CATEGORY: InferenceJobProductCategory = InferenceJobProductCategory::BeebleVideo; + const EXTERNAL_THIRD_PARTY: InferenceJobExternalThirdParty = InferenceJobExternalThirdParty::Beeble; + + let status = args.starting_job_status_override.unwrap_or(JobStatusPlus::Pending); + + let maybe_frontend_failure_category_str = args.maybe_frontend_failure_category + .map(|c| c.to_str()); + + let maybe_truncated_failure_reason = args.maybe_failure_reason + .map(|s| if s.len() > 255 { &s[..255] } else { s }); + + let query = sqlx::query!( + r#" +INSERT INTO generic_inference_jobs +SET + token = ?, + uuid_idempotency_token = ?, + + job_type = ?, + + maybe_external_third_party = ?, + maybe_external_third_party_id = ?, + + product_category = ?, + inference_category = ?, + + maybe_model_type = NULL, + maybe_model_token = NULL, + + maybe_input_source_token = NULL, + maybe_input_source_token_type = NULL, + + maybe_download_url = NULL, + maybe_cover_image_media_file_token = NULL, + + maybe_prompt_token = ?, + + maybe_raw_inference_text = NULL, + + maybe_inference_args = ?, + + maybe_creator_user_token = ?, + maybe_creator_anonymous_visitor_token = ?, + creator_ip_address = ?, + creator_set_visibility = ?, + + priority_level = 0, + is_keepalive_required = FALSE, + max_duration_seconds = 0, + + is_debug_request = FALSE, + maybe_routing_tag = NULL, + + maybe_debug_log_event_token = ?, + + frontend_failure_category = ?, + failure_reason = ?, + + status = ? + "#, + args.apriori_job_token.as_str(), + args.uuid_idempotency_token, + + JOB_TYPE.to_str(), + + EXTERNAL_THIRD_PARTY.to_str(), + args.maybe_external_third_party_id, + + PRODUCT_CATEGORY.to_str(), + INFERENCE_CATEGORY.to_str(), + + args.maybe_prompt_token.map(|t| t.to_string()), + + serialized_args_payload, + + args.maybe_creator_user_token.map(|t| t.to_string()), + args.maybe_avt_token.map(|t| t.to_string()), + args.creator_ip_address, + args.creator_set_visibility.to_str(), + + args.maybe_debug_log_event_token.map(|t| t.as_str()), + + maybe_frontend_failure_category_str, + maybe_truncated_failure_reason, + + status.to_str(), + ); + + let query_result = query.execute(args.mysql_executor) + .await; + + let record_id = match query_result { + Err(err) => return Err(DatabaseQueryError::from(err)), + Ok(res) => res.last_insert_id(), + }; + + info!("Insert generic inference job for Beeble queue: {} with record ID {}", args.apriori_job_token, record_id); + + Ok(args.apriori_job_token.clone()) +} diff --git a/crates/schema/database/mysql_queries/src/queries/generic_inference/beeble/mod.rs b/crates/schema/database/mysql_queries/src/queries/generic_inference/beeble/mod.rs new file mode 100644 index 0000000000..8e22648705 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/generic_inference/beeble/mod.rs @@ -0,0 +1 @@ +pub mod insert_generic_inference_job_for_beeble_queue_with_apriori_job_token; diff --git a/crates/schema/database/mysql_queries/src/queries/generic_inference/mod.rs b/crates/schema/database/mysql_queries/src/queries/generic_inference/mod.rs index 43c4342399..a60d53adac 100644 --- a/crates/schema/database/mysql_queries/src/queries/generic_inference/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/generic_inference/mod.rs @@ -1,3 +1,4 @@ +pub mod beeble; pub mod fal; pub mod job; pub mod moderation; diff --git a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_external_third_party.rs b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_external_third_party.rs index ddda651863..6cf1a2351b 100644 --- a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_external_third_party.rs +++ b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_external_third_party.rs @@ -12,6 +12,10 @@ use strum::EnumIter; #[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize, Default, utoipa::ToSchema)] #[serde(rename_all = "snake_case")] pub enum InferenceJobExternalThirdParty { + /// Beeble jobs + #[serde(rename = "beeble")] + Beeble, + /// Fal jobs #[serde(rename = "fal")] #[default] @@ -34,6 +38,7 @@ impl_mysql_enum_coders!(InferenceJobExternalThirdParty); impl InferenceJobExternalThirdParty { pub fn to_str(&self) -> &'static str { match self { + Self::Beeble => "beeble", Self::Fal => "fal", Self::Seedance2Pro => "seedance2pro", Self::Worldlabs => "worldlabs", @@ -42,6 +47,7 @@ impl InferenceJobExternalThirdParty { pub fn from_str(value: &str) -> Result { match value { + "beeble" => Ok(Self::Beeble), "fal" => Ok(Self::Fal), "seedance2pro" => Ok(Self::Seedance2Pro), "worldlabs" => Ok(Self::Worldlabs), @@ -53,6 +59,7 @@ impl InferenceJobExternalThirdParty { // NB: BTreeSet is sorted // NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc. BTreeSet::from([ + Self::Beeble, Self::Fal, Self::Seedance2Pro, Self::Worldlabs, @@ -70,6 +77,7 @@ mod tests { #[test] fn test_serialization() { + assert_serialization(InferenceJobExternalThirdParty::Beeble, "beeble"); assert_serialization(InferenceJobExternalThirdParty::Fal, "fal"); assert_serialization(InferenceJobExternalThirdParty::Seedance2Pro, "seedance2pro"); assert_serialization(InferenceJobExternalThirdParty::Worldlabs, "worldlabs"); @@ -77,6 +85,7 @@ mod tests { #[test] fn to_str() { + assert_eq!(InferenceJobExternalThirdParty::Beeble.to_str(), "beeble"); assert_eq!(InferenceJobExternalThirdParty::Fal.to_str(), "fal"); assert_eq!(InferenceJobExternalThirdParty::Seedance2Pro.to_str(), "seedance2pro"); assert_eq!(InferenceJobExternalThirdParty::Worldlabs.to_str(), "worldlabs"); @@ -84,6 +93,7 @@ mod tests { #[test] fn from_str() { + assert_eq!(InferenceJobExternalThirdParty::from_str("beeble").unwrap(), InferenceJobExternalThirdParty::Beeble); assert_eq!(InferenceJobExternalThirdParty::from_str("fal").unwrap(), InferenceJobExternalThirdParty::Fal); assert_eq!(InferenceJobExternalThirdParty::from_str("seedance2pro").unwrap(), InferenceJobExternalThirdParty::Seedance2Pro); assert_eq!(InferenceJobExternalThirdParty::from_str("worldlabs").unwrap(), InferenceJobExternalThirdParty::Worldlabs); @@ -92,7 +102,7 @@ mod tests { #[test] fn all_variants() { // Static check - const EXPECTED_COUNT : usize = 3; + const EXPECTED_COUNT : usize = 4; assert_eq!(InferenceJobExternalThirdParty::all_variants().len(), EXPECTED_COUNT); assert_eq!(InferenceJobExternalThirdParty::iter().len(), EXPECTED_COUNT); diff --git a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_product_category.rs b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_product_category.rs index b2a6182d4b..755a888238 100644 --- a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_product_category.rs +++ b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_product_category.rs @@ -30,6 +30,11 @@ pub enum InferenceJobProductCategory { FalObject, FalBgRemoval, + // =============== BEEBLE =============== + + #[serde(rename = "beeble_video")] + BeebleVideo, + // =============== SEEDANCE 2 PRO =============== #[serde(rename = "seedance2pro_video")] @@ -139,6 +144,7 @@ impl InferenceJobProductCategory { Self::FalVideo => "fal_video", Self::FalObject => "fal_object", Self::FalBgRemoval => "fal_bg_removal", + Self::BeebleVideo => "beeble_video", Self::Seedance2ProVideo => "seedance2pro_video", Self::Seedance2ProCharacter => "seedance2pro_character", Self::WorldlabsSplat => "worldlabs_splat", @@ -174,6 +180,7 @@ impl InferenceJobProductCategory { "fal_video" => Ok(Self::FalVideo), "fal_object" => Ok(Self::FalObject), "fal_bg_removal" => Ok(Self::FalBgRemoval), + "beeble_video" => Ok(Self::BeebleVideo), "seedance2pro_video" => Ok(Self::Seedance2ProVideo), "seedance2pro_character" => Ok(Self::Seedance2ProCharacter), "worldlabs_splat" => Ok(Self::WorldlabsSplat), @@ -212,6 +219,7 @@ impl InferenceJobProductCategory { Self::FalVideo, Self::FalObject, Self::FalBgRemoval, + Self::BeebleVideo, Self::Seedance2ProVideo, Self::Seedance2ProCharacter, Self::WorldlabsSplat, @@ -256,6 +264,7 @@ mod tests { assert_serialization(InferenceJobProductCategory::FalVideo, "fal_video"); assert_serialization(InferenceJobProductCategory::FalObject, "fal_object"); assert_serialization(InferenceJobProductCategory::FalBgRemoval, "fal_bg_removal"); + assert_serialization(InferenceJobProductCategory::BeebleVideo, "beeble_video"); assert_serialization(InferenceJobProductCategory::Seedance2ProVideo, "seedance2pro_video"); assert_serialization(InferenceJobProductCategory::Seedance2ProCharacter, "seedance2pro_character"); assert_serialization(InferenceJobProductCategory::WorldlabsSplat, "worldlabs_splat"); @@ -290,6 +299,7 @@ mod tests { assert_eq!(InferenceJobProductCategory::FalVideo.to_str(), "fal_video"); assert_eq!(InferenceJobProductCategory::FalObject.to_str(), "fal_object"); assert_eq!(InferenceJobProductCategory::FalBgRemoval.to_str(), "fal_bg_removal"); + assert_eq!(InferenceJobProductCategory::BeebleVideo.to_str(), "beeble_video"); assert_eq!(InferenceJobProductCategory::Seedance2ProVideo.to_str(), "seedance2pro_video"); assert_eq!(InferenceJobProductCategory::Seedance2ProCharacter.to_str(), "seedance2pro_character"); assert_eq!(InferenceJobProductCategory::WorldlabsSplat.to_str(), "worldlabs_splat"); @@ -323,6 +333,7 @@ mod tests { assert_eq!(InferenceJobProductCategory::from_str("fal_video").unwrap(), InferenceJobProductCategory::FalVideo); assert_eq!(InferenceJobProductCategory::from_str("fal_object").unwrap(), InferenceJobProductCategory::FalObject); assert_eq!(InferenceJobProductCategory::from_str("fal_bg_removal").unwrap(), InferenceJobProductCategory::FalBgRemoval); + assert_eq!(InferenceJobProductCategory::from_str("beeble_video").unwrap(), InferenceJobProductCategory::BeebleVideo); assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_video").unwrap(), InferenceJobProductCategory::Seedance2ProVideo); assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_character").unwrap(), InferenceJobProductCategory::Seedance2ProCharacter); assert_eq!(InferenceJobProductCategory::from_str("worldlabs_splat").unwrap(), InferenceJobProductCategory::WorldlabsSplat); @@ -352,7 +363,7 @@ mod tests { #[test] fn all_variants() { // Static check - const EXPECTED_COUNT : usize = 30; + const EXPECTED_COUNT : usize = 31; assert_eq!(InferenceJobProductCategory::all_variants().len(), EXPECTED_COUNT); assert_eq!(InferenceJobProductCategory::iter().len(), EXPECTED_COUNT); diff --git a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_type.rs b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_type.rs index 9990be8360..cb74bdead9 100644 --- a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_type.rs +++ b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_type.rs @@ -27,6 +27,10 @@ pub enum InferenceJobType { #[serde(rename = "seedance2pro_character")] Seedance2ProCharacter, + /// Beeble jobs. We poll for results. + #[serde(rename = "beeble_queue")] + BeebleQueue, + /// World Labs jobs. We poll for results. #[serde(rename = "worldlabs_queue")] WorldlabsQueue, @@ -122,6 +126,7 @@ impl InferenceJobType { pub fn to_str(&self) -> &'static str { match self { Self::FalQueue => "fal_queue", + Self::BeebleQueue => "beeble_queue", Self::Seedance2ProQueue => "seedance2pro_queue", Self::Seedance2ProCharacter => "seedance2pro_character", Self::WorldlabsQueue => "worldlabs_queue", @@ -151,6 +156,7 @@ impl InferenceJobType { pub fn from_str(value: &str) -> Result { match value { "fal_queue" => Ok(Self::FalQueue), + "beeble_queue" => Ok(Self::BeebleQueue), "seedance2pro_queue" => Ok(Self::Seedance2ProQueue), "seedance2pro_character" => Ok(Self::Seedance2ProCharacter), "worldlabs_queue" => Ok(Self::WorldlabsQueue), @@ -183,6 +189,7 @@ impl InferenceJobType { // NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc. BTreeSet::from([ Self::FalQueue, + Self::BeebleQueue, Self::Seedance2ProQueue, Self::Seedance2ProCharacter, Self::WorldlabsQueue, @@ -226,6 +233,7 @@ mod tests { #[test] fn test_serialization() { assert_serialization(InferenceJobType::FalQueue, "fal_queue"); + assert_serialization(InferenceJobType::BeebleQueue, "beeble_queue"); assert_serialization(InferenceJobType::Seedance2ProQueue, "seedance2pro_queue"); assert_serialization(InferenceJobType::Seedance2ProCharacter, "seedance2pro_character"); assert_serialization(InferenceJobType::WorldlabsQueue, "worldlabs_queue"); @@ -254,6 +262,7 @@ mod tests { #[test] fn to_str() { assert_eq!(InferenceJobType::FalQueue.to_str(), "fal_queue"); + assert_eq!(InferenceJobType::BeebleQueue.to_str(), "beeble_queue"); assert_eq!(InferenceJobType::Seedance2ProQueue.to_str(), "seedance2pro_queue"); assert_eq!(InferenceJobType::Seedance2ProCharacter.to_str(), "seedance2pro_character"); assert_eq!(InferenceJobType::WorldlabsQueue.to_str(), "worldlabs_queue"); @@ -282,6 +291,7 @@ mod tests { #[test] fn from_str() { assert_eq!(InferenceJobType::from_str("fal_queue").unwrap(), InferenceJobType::FalQueue); + assert_eq!(InferenceJobType::from_str("beeble_queue").unwrap(), InferenceJobType::BeebleQueue); assert_eq!(InferenceJobType::from_str("seedance2pro_queue").unwrap(), InferenceJobType::Seedance2ProQueue); assert_eq!(InferenceJobType::from_str("seedance2pro_character").unwrap(), InferenceJobType::Seedance2ProCharacter); assert_eq!(InferenceJobType::from_str("worldlabs_queue").unwrap(), InferenceJobType::WorldlabsQueue); @@ -310,7 +320,7 @@ mod tests { #[test] fn all_variants() { // Static check - const EXPECTED_COUNT : usize = 24; + const EXPECTED_COUNT : usize = 25; assert_eq!(InferenceJobType::all_variants().len(), EXPECTED_COUNT); assert_eq!(InferenceJobType::iter().len(), EXPECTED_COUNT); diff --git a/crates/schema/public/enums/src/common/generation/common_model_type.rs b/crates/schema/public/enums/src/common/generation/common_model_type.rs index 7ef7a34a08..1d25d14f93 100644 --- a/crates/schema/public/enums/src/common/generation/common_model_type.rs +++ b/crates/schema/public/enums/src/common/generation/common_model_type.rs @@ -144,6 +144,8 @@ pub enum CommonModelType { Veo3p1, #[serde(rename = "veo_3p1_fast")] Veo3p1Fast, + #[serde(rename = "switch_x")] + SwitchX, // 3D Object generation models #[serde(rename = "hunyuan_3d_2p0")] @@ -224,6 +226,7 @@ impl CommonModelType { Self::Veo3Fast => "veo_3_fast", Self::Veo3p1 => "veo_3p1", Self::Veo3p1Fast => "veo_3p1_fast", + Self::SwitchX => "switch_x", // 3D Object generation models Self::Hunyuan3d2_0 => "hunyuan_3d_2p0", @@ -293,6 +296,7 @@ impl CommonModelType { "veo_3_fast" => Ok(Self::Veo3Fast), "veo_3p1" => Ok(Self::Veo3p1), "veo_3p1_fast" => Ok(Self::Veo3p1Fast), + "switch_x" => Ok(Self::SwitchX), // 3D Object generation models "hunyuan_3d_2p0" => Ok(Self::Hunyuan3d2_0), @@ -366,6 +370,7 @@ impl CommonModelType { Self::Veo3Fast, Self::Veo3p1, Self::Veo3p1Fast, + Self::SwitchX, // 3D Object generation models Self::Hunyuan3d2_0, @@ -436,6 +441,7 @@ impl CommonModelType { Self::Veo3Fast => CommonModelClass::Video, Self::Veo3p1 => CommonModelClass::Video, Self::Veo3p1Fast => CommonModelClass::Video, + Self::SwitchX => CommonModelClass::Video, // 3D Object generation models (mesh) Self::Hunyuan3d2_0 => CommonModelClass::DimensionalMesh, @@ -515,6 +521,7 @@ mod tests { assert_serialization(CommonModelType::Veo3Fast, "veo_3_fast"); assert_serialization(CommonModelType::Veo3p1, "veo_3p1"); assert_serialization(CommonModelType::Veo3p1Fast, "veo_3p1_fast"); + assert_serialization(CommonModelType::SwitchX, "switch_x"); // 3D Object generation models assert_serialization(CommonModelType::Hunyuan3d2_0, "hunyuan_3d_2p0"); assert_serialization(CommonModelType::Hunyuan3d2_1, "hunyuan_3d_2p1"); @@ -581,6 +588,7 @@ mod tests { assert_eq!(CommonModelType::Veo3Fast.to_str(), "veo_3_fast"); assert_eq!(CommonModelType::Veo3p1.to_str(), "veo_3p1"); assert_eq!(CommonModelType::Veo3p1Fast.to_str(), "veo_3p1_fast"); + assert_eq!(CommonModelType::SwitchX.to_str(), "switch_x"); // 3D Object generation models assert_eq!(CommonModelType::Hunyuan3d2_0.to_str(), "hunyuan_3d_2p0"); @@ -647,7 +655,8 @@ mod tests { assert_eq!(CommonModelType::from_str("veo_3_fast").unwrap(), CommonModelType::Veo3Fast); assert_eq!(CommonModelType::from_str("veo_3p1").unwrap(), CommonModelType::Veo3p1); assert_eq!(CommonModelType::from_str("veo_3p1_fast").unwrap(), CommonModelType::Veo3p1Fast); - + assert_eq!(CommonModelType::from_str("switch_x").unwrap(), CommonModelType::SwitchX); + // 3D Object generation models assert_eq!(CommonModelType::from_str("hunyuan_3d_2p0").unwrap(), CommonModelType::Hunyuan3d2_0); assert_eq!(CommonModelType::from_str("hunyuan_3d_2p1").unwrap(), CommonModelType::Hunyuan3d2_1); @@ -660,7 +669,7 @@ mod tests { #[test] fn all_variants() { let mut variants = CommonModelType::all_variants(); - assert_eq!(variants.len(), 57); + assert_eq!(variants.len(), 58); // Image models assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Dev)); assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Schnell)); @@ -715,6 +724,7 @@ mod tests { assert_eq!(variants.pop_first(), Some(CommonModelType::Veo3Fast)); assert_eq!(variants.pop_first(), Some(CommonModelType::Veo3p1)); assert_eq!(variants.pop_first(), Some(CommonModelType::Veo3p1Fast)); + assert_eq!(variants.pop_first(), Some(CommonModelType::SwitchX)); // 3D Object generation models assert_eq!(variants.pop_first(), Some(CommonModelType::Hunyuan3d2_0)); assert_eq!(variants.pop_first(), Some(CommonModelType::Hunyuan3d2_1)); diff --git a/crates/service/web/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml index 0ddf9a7e34..ad879b10f8 100644 --- a/crates/service/web/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -50,6 +50,7 @@ datetimes = { path = "../../../lib/data/datetimes" } easyenv.workspace = true elasticsearch_schema = { path = "../../../schema/database/elasticsearch_schema" } enums.workspace = true +beeble_client.workspace = true error_utils.workspace = true errors.workspace = true fal_client.workspace = true 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 bf9cf2a95b..41a4880537 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -85,6 +85,7 @@ use crate::http_server::endpoints::media_files::upsert_upload::write_engine_asse use crate::http_server::endpoints::media_files::upsert_upload::write_error::MediaFileWriteError; use crate::http_server::endpoints::media_files::upsert_upload::write_scene_file::write_scene_file_media_file_handler::*; use crate::http_server::endpoints::model_download::enqueue_gptsovits_model_download_handler::*; +use artcraft_api_defs::generate::video::edit::beeble_switchx_edit_video::*; use crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_token_handler::*; use crate::http_server::endpoints::moderation::jobs::moderation_get_job_by_token_handler::*; use crate::http_server::endpoints::moderation::user_feature_flags::moderator_edit_user_feature_flags_handler::*; @@ -365,6 +366,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux crate::http_server::endpoints::generate::video::image::generate_kling_2_1_master_video_handler::generate_kling_2_1_master_video_handler, crate::http_server::endpoints::generate::video::image::generate_kling_2_1_pro_video_handler::generate_kling_2_1_pro_video_handler, crate::http_server::endpoints::generate::video::image::generate_seedance_1_0_lite_image_to_video_handler::generate_seedance_1_0_lite_image_to_video_handler, + crate::http_server::endpoints::generate::video::edit::beeble_switchx_edit_video_gen_handler::beeble_switchx_edit_video_gen_handler, crate::http_server::endpoints::generate::video::image::generate_veo_2_image_to_video_handler::generate_veo_2_image_to_video_handler, crate::http_server::endpoints::image_studio::upload::upload_snapshot_media_file_handler::upload_snapshot_media_file_handler, crate::http_server::endpoints::inference_job::delete::dismiss_finished_session_jobs_handler::dismiss_finished_session_jobs_handler, @@ -860,6 +862,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux GenerateSeedance10LiteImageToVideoResponse, GenerateSeedance10LiteResolution, GenerateVeo2AspectRatio, + BeebleSwitchXEditVideoRequest, + BeebleSwitchXEditVideoResponse, GenerateVeo2Duration, GenerateVeo2ImageToVideoRequest, GenerateVeo2ImageToVideoResponse, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/edit/beeble_switchx_edit_video_gen_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/edit/beeble_switchx_edit_video_gen_handler.rs new file mode 100644 index 0000000000..b8bcf83ca1 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/edit/beeble_switchx_edit_video_gen_handler.rs @@ -0,0 +1,309 @@ +use std::sync::Arc; + +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; +use log::{error, info, warn}; +use sqlx::Acquire; + +use artcraft_api_defs::generate::video::edit::beeble_switchx_edit_video::{ + BeebleSwitchXEditVideoRequest, BeebleSwitchXEditVideoResponse, +}; +use beeble_client::requests::create_upload_url::upload_bytes::upload_bytes_to_beeble; +use beeble_client::requests::start_generation::start_generation::{ + start_generation, BeebleAlphaMode, BeebleGenerationType, StartGenerationArgs, + StartGenerationRequest, +}; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::by_table::prompts::prompt_type::PromptType; +use enums::common::generation::common_generation_mode::CommonGenerationMode; +use enums::common::generation::common_model_type::CommonModelType; +use enums::common::generation_provider::GenerationProvider; +use enums::common::visibility::Visibility; +use http_server_common::request::get_request_ip::get_request_ip; +use mysql_queries::queries::generic_inference::beeble::insert_generic_inference_job_for_beeble_queue_with_apriori_job_token::{ + insert_generic_inference_job_for_beeble_queue_with_apriori_job_token, + InsertGenericInferenceForBeebleWithAprioriJobTokenArgs, +}; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::media_files::get::get_media_file::get_media_file_with_connection; +use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; + +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +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::http_download_url_to_bytes::http_download_url_to_bytes; + +/// Beeble SwitchX Video Edit +#[utoipa::path( + post, + tag = "Generate Videos", + path = "/v1/generate/video/edit/beeble_switchx", + request_body = BeebleSwitchXEditVideoRequest, + responses( + (status = 200, description = "Success", body = BeebleSwitchXEditVideoResponse), + (status = 400, description = "Bad input"), + (status = 401, description = "Unauthorized"), + (status = 402, description = "Payment required"), + (status = 500, description = "Server error"), + ), +)] +pub async fn beeble_switchx_edit_video_gen_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data>, +) -> Result, AdvancedCommonWebError> { + + payments_error_test(&request.prompt.as_deref().unwrap_or(""))?; + + // ==================== SESSION ==================== // + + 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); + AdvancedCommonWebError::from(e) + })?; + + let user_token = match maybe_user_session.as_ref() { + Some(session) => &session.user_token, + None => return Err(AdvancedCommonWebError::NotAuthorized), + }; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // ==================== VALIDATE INPUT ==================== // + + let source_video_media_token = request.source_video_media_token.as_ref() + .ok_or_else(|| { + AdvancedCommonWebError::BadInputWithSimpleMessage("source_video_media_token is required".to_string()) + })?; + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + AdvancedCommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string()) + })?; + + // ==================== LOOKUP MEDIA FILES ==================== // + + const IS_MOD: bool = false; + let media_domain = get_media_domain(&http_request); + + // Source video (required) + let source_video_file = get_media_file_with_connection( + source_video_media_token, IS_MOD, &mut mysql_connection, + ).await + .map_err(|err| { + warn!("Error looking up source video: {:?}", err); + AdvancedCommonWebError::from_anyhow_error(err) + })? + .ok_or(AdvancedCommonWebError::NotFound)?; + + let source_video_cdn_url = { + let path = MediaFileBucketPath::from_object_hash( + &source_video_file.public_bucket_directory_hash, + source_video_file.maybe_public_bucket_prefix.as_deref(), + source_video_file.maybe_public_bucket_extension.as_deref(), + ); + MediaLinksBuilder::from_media_path_and_env(media_domain, server_state.server_environment, &path) + .cdn_url.to_string() + }; + + // Reference image (optional) + let maybe_reference_image_cdn_url = if let Some(ref_token) = &request.reference_image_media_token { + let ref_file = get_media_file_with_connection(ref_token, IS_MOD, &mut mysql_connection) + .await + .map_err(|err| { + warn!("Error looking up reference image: {:?}", err); + AdvancedCommonWebError::from_anyhow_error(err) + })? + .ok_or(AdvancedCommonWebError::NotFound)?; + + let path = MediaFileBucketPath::from_object_hash( + &ref_file.public_bucket_directory_hash, + ref_file.maybe_public_bucket_prefix.as_deref(), + ref_file.maybe_public_bucket_extension.as_deref(), + ); + + Some( + MediaLinksBuilder::from_media_path_and_env(media_domain, server_state.server_environment, &path) + .cdn_url.to_string() + ) + } else { + None + }; + + // ==================== DOWNLOAD & UPLOAD TO BEEBLE ==================== // + + let beeble_api_key = &server_state.beeble.api_key; + + // Download + upload source video + info!("Downloading source video from CDN: {}", source_video_cdn_url); + let video_bytes = http_download_url_to_bytes(&source_video_cdn_url).await + .map_err(|err| { + error!("Failed to download source video: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + info!("Downloaded source video: {} bytes", video_bytes.len()); + + let video_extension = source_video_file.maybe_public_bucket_extension + .as_deref().unwrap_or("mp4"); + let video_filename = format!("{}.{}", source_video_media_token.as_str(), video_extension); + let video_content_type = match video_extension { + "mp4" => "video/mp4", + "mov" => "video/quicktime", + _ => "application/octet-stream", + }; + + let video_upload = upload_bytes_to_beeble( + beeble_api_key, &video_filename, video_content_type, video_bytes.to_vec(), + ).await.map_err(|err| { + error!("Beeble video upload failed: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + info!("Source video uploaded to Beeble: {}", video_upload.beeble_uri); + + // Download + upload reference image (if provided) + let maybe_reference_beeble_uri = if let Some(ref_cdn_url) = &maybe_reference_image_cdn_url { + info!("Downloading reference image from CDN: {}", ref_cdn_url); + let image_bytes = http_download_url_to_bytes(ref_cdn_url).await + .map_err(|err| { + error!("Failed to download reference image: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + info!("Downloaded reference image: {} bytes", image_bytes.len()); + + let ref_token = request.reference_image_media_token.as_ref().unwrap(); + let image_filename = format!("{}.jpg", ref_token.as_str()); + + let image_upload = upload_bytes_to_beeble( + beeble_api_key, &image_filename, "image/jpeg", image_bytes.to_vec(), + ).await.map_err(|err| { + error!("Beeble image upload failed: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + info!("Reference image uploaded to Beeble: {}", image_upload.beeble_uri); + Some(image_upload.beeble_uri) + } else { + None + }; + + // ==================== CALL BEEBLE GENERATE ==================== // + + let apriori_job_token = InferenceJobToken::generate(); + + let beeble_result = start_generation(StartGenerationArgs { + api_key: beeble_api_key.clone(), + request: StartGenerationRequest { + generation_type: BeebleGenerationType::Video, + source_uri: video_upload.beeble_uri, + alpha_mode: BeebleAlphaMode::Auto, + prompt: request.prompt.clone(), + reference_image_uri: maybe_reference_beeble_uri, + alpha_uri: None, + max_resolution: Some(1080), + callback_url: None, + idempotency_key: Some(request.uuid_idempotency_token.clone()), + }, + }).await.map_err(|err| { + error!("Beeble start_generation failed: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + let external_job_id = &beeble_result.id; + info!("Beeble job started: id={}, status={}", external_job_id, beeble_result.status); + + // ==================== DB TRANSACTION ==================== // + + let ip_address = get_request_ip(&http_request); + + let mut transaction = mysql_connection.begin().await.map_err(|err| { + error!("Error starting MySQL transaction: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + // -- Prompt -- + let prompt_token = match insert_prompt(InsertPromptArgs { + maybe_apriori_prompt_token: None, + prompt_type: PromptType::ArtcraftApp, + maybe_creator_user_token: Some(user_token), + maybe_model_type: Some(CommonModelType::SwitchX), + maybe_generation_provider: Some(GenerationProvider::Artcraft), + maybe_positive_prompt: request.prompt.as_deref(), + maybe_negative_prompt: None, + maybe_other_args: None, + maybe_generation_mode: Some(CommonGenerationMode::Edit), + maybe_aspect_ratio: None, + maybe_resolution: None, + maybe_batch_count: None, + maybe_generate_audio: None, + maybe_duration_seconds: None, + creator_ip_address: &ip_address, + mysql_executor: &mut *transaction, + phantom: Default::default(), + }).await { + Ok(token) => Some(token), + Err(err) => { + warn!("Error inserting prompt: {:?}", err); + None + } + }; + + // -- Inference job -- + let db_result = insert_generic_inference_job_for_beeble_queue_with_apriori_job_token( + InsertGenericInferenceForBeebleWithAprioriJobTokenArgs { + apriori_job_token: &apriori_job_token, + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: external_job_id, + maybe_inference_args: None, + maybe_prompt_token: prompt_token.as_ref(), + maybe_creator_user_token: Some(user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + maybe_debug_log_event_token: None, + starting_job_status_override: None, + maybe_frontend_failure_category: None, + maybe_failure_reason: None, + mysql_executor: &mut *transaction, + phantom: Default::default(), + } + ).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + error!("Error inserting inference job: {:?}", err); + return Err(AdvancedCommonWebError::from_error(err)); + } + }; + + transaction.commit().await.map_err(|err| { + error!("Error committing transaction: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + Ok(Json(BeebleSwitchXEditVideoResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/edit/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/edit/mod.rs new file mode 100644 index 0000000000..3810ce6a34 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/edit/mod.rs @@ -0,0 +1 @@ +pub mod beeble_switchx_edit_video_gen_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/mod.rs index 158c74d131..2f329668b0 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/mod.rs @@ -1,2 +1,3 @@ +pub mod edit; pub mod image; pub mod multi_function; \ No newline at end of file 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 9c6bf9b0dc..91b96d2555 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 @@ -1,3 +1,4 @@ +use crate::http_server::endpoints::generate::video::edit::beeble_switchx_edit_video_gen_handler::beeble_switchx_edit_video_gen_handler; use crate::http_server::endpoints::generate::cost_estimate::image::estimate_image_cost_handler::estimate_image_cost_handler; use crate::http_server::endpoints::generate::cost_estimate::splat::estimate_splat_cost_handler::estimate_splat_cost_handler; use crate::http_server::endpoints::generate::cost_estimate::video::estimate_video_cost_handler::estimate_video_cost_handler; @@ -251,6 +252,12 @@ where .route(web::head().to(|| HttpResponse::Ok())) ) ) + .service(web::scope("/edit") + .service(web::resource("/beeble_switchx") + .route(web::post().to(beeble_switchx_edit_video_gen_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + ) .service(web::scope("/object") .service(web::scope("/multi_function") .service(web::resource("/hunyuan3d_v3") diff --git a/crates/service/web/storyteller_web/src/main.rs b/crates/service/web/storyteller_web/src/main.rs index bf2083bac0..ad09789439 100644 --- a/crates/service/web/storyteller_web/src/main.rs +++ b/crates/service/web/storyteller_web/src/main.rs @@ -80,7 +80,7 @@ use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator use crate::startup::build_pager::build_pager; use crate::state::certs::google_sign_in_cert::GoogleSignInCert; use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache; -use crate::state::server_state::{DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData, InMemoryCaches, OpenAiData, ResendData, Seedance2ProData, ServerInfo, ServerState, StaticFeatureFlags, StripeSettings, TrollBans, WorldLabsData}; +use crate::state::server_state::{BeebleData, DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData, InMemoryCaches, OpenAiData, ResendData, Seedance2ProData, ServerInfo, ServerState, StaticFeatureFlags, StripeSettings, TrollBans, WorldLabsData}; use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus; use crate::threads::db_health_checker_thread::db_health_checker_thread::db_health_checker_thread; use crate::threads::poll_ip_banlist_thread::poll_ip_bans; @@ -475,6 +475,11 @@ async fn main() -> AnyhowResult<()> { sort_key_crypto, opaque_cursors: opaque_cursor_encoder, static_api_token_set, + beeble: BeebleData { + api_key: beeble_client::creds::beeble_api_key::BeebleApiKey::new( + easyenv::get_env_string_required("BEEBLE_API_KEY")?, + ), + }, fal: FalData { api_key: fal_api_key, webhook_url: fal_webhook_url, diff --git a/crates/service/web/storyteller_web/src/state/server_state.rs b/crates/service/web/storyteller_web/src/state/server_state.rs index 1f4c1263d2..75bc0d675e 100644 --- a/crates/service/web/storyteller_web/src/state/server_state.rs +++ b/crates/service/web/storyteller_web/src/state/server_state.rs @@ -25,6 +25,7 @@ use billing_component::stripe::stripe_config::StripeConfig; use chrono::{DateTime, Utc}; use cloud_storage::bucket_client::BucketClient; use elasticsearch::Elasticsearch; +use beeble_client::creds::beeble_api_key::BeebleApiKey; use fal_client::creds::fal_api_key::FalApiKey; use memory_caching::arc_ttl_sieve::ArcTtlSieve; use memory_caching::single_item_ttl_cache::SingleItemTtlCache; @@ -95,6 +96,8 @@ pub struct ServerState { pub fal: FalData, + pub beeble: BeebleData, + pub seedance2pro: Seedance2ProData, pub openai: OpenAiData, @@ -321,6 +324,12 @@ pub struct FalData { pub webhook_url: String, } +/// Beeble SwitchX integration +#[derive(Clone)] +pub struct BeebleData { + pub api_key: BeebleApiKey, +} + /// Seedance 2 Pro integration #[derive(Clone)] pub struct Seedance2ProData {