new endpoint

This commit is contained in:
Brandon Thomas
2026-05-08 13:22:36 -04:00
parent c78442cf71
commit 488ef32cc3
23 changed files with 649 additions and 6 deletions
@@ -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<MediaFileToken>,
pub reference_image_media_token: Option<MediaFileToken>,
pub prompt: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BeebleSwitchXEditVideoResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1 @@
pub mod beeble_switchx_edit_video;
@@ -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;
@@ -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<BeebleSwitchXEditVideoResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
BEEBLE_SWITCHX_EDIT_VIDEO_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1 @@
pub mod beeble_switchx_edit_video;
@@ -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;
@@ -1,2 +1,3 @@
pub mod create_upload_url;
mod request_types;
pub mod upload_bytes;
@@ -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<u8>,
) -> Result<CreateUploadUrlSuccess, BeebleError> {
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)
}
@@ -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<GenericInferenceArgs>,
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<JobStatusPlus>,
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
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<InferenceJobToken, DatabaseQueryError>
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())
}
@@ -0,0 +1 @@
pub mod insert_generic_inference_job_for_beeble_queue_with_apriori_job_token;
@@ -1,3 +1,4 @@
pub mod beeble;
pub mod fal;
pub mod job;
pub mod moderation;
@@ -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<Self, String> {
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);
@@ -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);
@@ -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<Self, String> {
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);
@@ -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));
@@ -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
@@ -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,
@@ -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<BeebleSwitchXEditVideoRequest>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<BeebleSwitchXEditVideoResponse>, 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,
}))
}
@@ -0,0 +1 @@
pub mod beeble_switchx_edit_video_gen_handler;
@@ -1,2 +1,3 @@
pub mod edit;
pub mod image;
pub mod multi_function;
@@ -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")
@@ -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,
@@ -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 {