From 1d1f8a9017f16a9c63331b59752e96099927e9bb Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 19 Jan 2026 02:52:35 -0500 Subject: [PATCH] Hunyuan 3D v3.0 support (#986) --- .../src/generate/object/mod.rs | 3 +- .../hunyuan3d_v3_multi_function_object_gen.rs | 84 ++++ .../src/generate/object/multi_function/mod.rs | 1 + .../src/endpoints/generate/object/mod.rs | 3 +- .../hunyuan3d_v3_multi_function_object_gen.rs | 18 + .../generate/object/multi_function/mod.rs | 1 + ...nqueue_hunyuan3d_v3_image_to_3d_webhook.rs | 148 +++++++ ...queue_hunyuan3d_v3_sketch_to_3d_webhook.rs | 133 ++++++ ...enqueue_hunyuan3d_v3_text_to_3d_webhook.rs | 129 ++++++ .../src/requests/webhook/object/mod.rs | 5 +- .../public/enums/src/common/model_type.rs | 11 +- .../endpoints/generate/object/mod.rs | 3 +- ...yuan3d_v3_multi_function_object_handler.rs | 400 ++++++++++++++++++ .../generate/object/multi_function/mod.rs | 1 + .../application_routes/generate_routes.rs | 7 + .../hunyuan3d/hunyuan3d_v3_image_to_3d.rs | 63 +++ .../hunyuan3d/hunyuan3d_v3_sketch_to_3d.rs | 46 ++ .../hunyuan3d/hunyuan3d_v3_text_to_3d.rs | 43 ++ .../src/endpoints/fal_ai/hunyuan3d/mod.rs | 42 ++ 19 files changed, 1136 insertions(+), 5 deletions(-) create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/generate/object/multi_function/hunyuan3d_v3_multi_function_object_gen.rs create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/generate/object/multi_function/mod.rs create mode 100644 crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/object/multi_function/hunyuan3d_v3_multi_function_object_gen.rs create mode 100644 crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/object/multi_function/mod.rs create mode 100644 crates/api_clients/fal_client/src/requests/webhook/object/enqueue_hunyuan3d_v3_image_to_3d_webhook.rs create mode 100644 crates/api_clients/fal_client/src/requests/webhook/object/enqueue_hunyuan3d_v3_sketch_to_3d_webhook.rs create mode 100644 crates/api_clients/fal_client/src/requests/webhook/object/enqueue_hunyuan3d_v3_text_to_3d_webhook.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/multi_function/generate_hunyuan3d_v3_multi_function_object_handler.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/multi_function/mod.rs create mode 100644 crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/hunyuan3d_v3_image_to_3d.rs create mode 100644 crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/hunyuan3d_v3_sketch_to_3d.rs create mode 100644 crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/hunyuan3d_v3_text_to_3d.rs diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/object/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/object/mod.rs index b3070c666d..582e9c131f 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/object/mod.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/object/mod.rs @@ -1,2 +1,3 @@ pub mod generate_hunyuan_2_0_image_to_3d; -pub mod generate_hunyuan_2_1_image_to_3d; \ No newline at end of file +pub mod generate_hunyuan_2_1_image_to_3d; +pub mod multi_function; \ No newline at end of file diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/object/multi_function/hunyuan3d_v3_multi_function_object_gen.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/object/multi_function/hunyuan3d_v3_multi_function_object_gen.rs new file mode 100644 index 0000000000..70d0c62f64 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/object/multi_function/hunyuan3d_v3_multi_function_object_gen.rs @@ -0,0 +1,84 @@ +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const HUNYUAN3D_V3_MULTI_FUNCTION_OBJECT_GEN_PATH: &str = "/v1/generate/object/multi_function/hunyuan3d_v3"; + +/// Hunyuan 3D v3 Multi-Function Request +/// +/// This endpoint combines three modes: +/// - Text-to-3D: Provide only a prompt (no image_media_token) +/// - Image-to-3D: Provide an image_media_token (no prompt or empty prompt) +/// - Sketch-to-3D: Provide both a prompt AND an image_media_token +#[derive(Serialize, Deserialize, ToSchema, Debug)] +pub struct Hunyuan3dV3MultiFunctionObjectGenRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Text prompt for generation. + /// - Required for text-to-3d mode + /// - Required for sketch-to-3d mode + /// - Ignored for image-to-3d mode + pub prompt: Option, + + /// Primary image media token. + /// - Required for image-to-3d mode + /// - Required for sketch-to-3d mode + /// - Not used for text-to-3d mode + pub image_media_token: Option, + + /// Back view image for multi-view image-to-3d mode. + /// Only applicable when image_media_token is provided without a prompt. + pub back_image_media_token: Option, + + /// Left view image for multi-view image-to-3d mode. + /// Only applicable when image_media_token is provided without a prompt. + pub left_image_media_token: Option, + + /// Right view image for multi-view image-to-3d mode. + /// Only applicable when image_media_token is provided without a prompt. + pub right_image_media_token: Option, + + /// Target face count for the generated mesh. + /// Optional - if not provided, the model will use its default. + pub face_count: Option, + + /// Type of generation to perform. + /// Default is Normal. + pub generate_type: Option, + + /// Type of polygon to use in the generated mesh. + /// Default is Triangle. + pub polygon_type: Option, + + /// Whether to enable PBR (Physically Based Rendering) materials. + /// Default is false. + pub enable_pbr: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum Hunyuan3dV3GenerateType { + /// Standard generation quality (default) + Normal, + /// Low polygon count for game assets + LowPoly, + /// Geometry only, no textures + Geometry, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum Hunyuan3dV3PolygonType { + /// Triangle polygons (default) + Triangle, + /// Quadrilateral polygons + Quadrilateral, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct Hunyuan3dV3MultiFunctionObjectGenResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/object/multi_function/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/object/multi_function/mod.rs new file mode 100644 index 0000000000..22285b9e3a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/object/multi_function/mod.rs @@ -0,0 +1 @@ +pub mod hunyuan3d_v3_multi_function_object_gen; diff --git a/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/object/mod.rs b/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/object/mod.rs index 9577941162..205c136ec8 100644 --- a/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/object/mod.rs +++ b/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/object/mod.rs @@ -1,2 +1,3 @@ pub mod generate_hunyuan_3d_2_0_image_to_3d; -pub mod generate_hunyuan_3d_2_1_image_to_3d; \ No newline at end of file +pub mod generate_hunyuan_3d_2_1_image_to_3d; +pub mod multi_function; \ No newline at end of file diff --git a/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/object/multi_function/hunyuan3d_v3_multi_function_object_gen.rs b/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/object/multi_function/hunyuan3d_v3_multi_function_object_gen.rs new file mode 100644 index 0000000000..0c8682cc3f --- /dev/null +++ b/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/object/multi_function/hunyuan3d_v3_multi_function_object_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::object::multi_function::hunyuan3d_v3_multi_function_object_gen::{Hunyuan3dV3MultiFunctionObjectGenRequest, Hunyuan3dV3MultiFunctionObjectGenResponse, HUNYUAN3D_V3_MULTI_FUNCTION_OBJECT_GEN_PATH}; + +pub async fn hunyuan3d_v3_multi_function_object_gen( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + request: Hunyuan3dV3MultiFunctionObjectGenRequest, +) -> Result { + Ok(basic_json_post_request( + api_host, + HUNYUAN3D_V3_MULTI_FUNCTION_OBJECT_GEN_PATH, + maybe_creds, + request, + ).await?) +} diff --git a/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/object/multi_function/mod.rs b/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/object/multi_function/mod.rs new file mode 100644 index 0000000000..22285b9e3a --- /dev/null +++ b/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/object/multi_function/mod.rs @@ -0,0 +1 @@ +pub mod hunyuan3d_v3_multi_function_object_gen; diff --git a/crates/api_clients/fal_client/src/requests/webhook/object/enqueue_hunyuan3d_v3_image_to_3d_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/object/enqueue_hunyuan3d_v3_image_to_3d_webhook.rs new file mode 100644 index 0000000000..c2490f4442 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/object/enqueue_hunyuan3d_v3_image_to_3d_webhook.rs @@ -0,0 +1,148 @@ +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::hunyuan3d::hunyuan3d_v3_image_to_3d::{hunyuan3d_v3_image_to_3d, Hunyuan3dV3ImageTo3dInput}; + +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueHunyuan3dV3ImageTo3dArgs<'a, R: IntoUrl> { + // Request required + pub image_url: String, + + // Optional args + pub back_image_url : Option, + pub left_image_url : Option, + pub right_image_url : Option, + pub face_count: Option, + pub generate_type: Option, + pub polygon_type: Option, + pub enable_pbr: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueHunyuan3dV3ImageTo3dGenerateType { + Normal, + LowPoly, + Geometry, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueHunyuan3dV3ImageTo3dPolygonType { + Triangle, + Quadrilateral, +} + +impl FalRequestCostCalculator for EnqueueHunyuan3dV3ImageTo3dArgs<'_, R> { + fn calculate_cost_in_cents(&self) -> UsdCents { + // "Your request will cost $0.375 per generation. + // For $1.00, you can run this model 2 times. + // Generation types: Normal costs $0.375, + // LowPoly costs $0.45, + // Geometry costs $0.225. + // Enabling PBR materials adds $0.15. + // Using multi-view images (back/left/right) adds $0.15. + // Custom face count adds $0.15. + let mut cost = match self.generate_type{ + None => 38, // Round up from $0.375 + Some(EnqueueHunyuan3dV3ImageTo3dGenerateType::Normal) => 38, // Round up + Some(EnqueueHunyuan3dV3ImageTo3dGenerateType::Geometry) => 23, // Round up + Some(EnqueueHunyuan3dV3ImageTo3dGenerateType::LowPoly) => 45, + }; + if self.enable_pbr.unwrap_or(false) { + cost += 15; + } + if self.face_count.is_some() { + cost += 15; + } + let use_multi_view = self.left_image_url.is_some() + || self.right_image_url.is_some() + || self.back_image_url.is_some(); + if use_multi_view { + cost += 15; + } + cost + } +} + + +/// Veo 3.1 Image-to-Video +/// https://fal.ai/models/fal-ai/veo3.1/image-to-video +pub async fn enqueue_hunyuan3d_v3_image_to_3d_webhook( + args: EnqueueHunyuan3dV3ImageTo3dArgs<'_, R> +) -> Result { + + let generate_type= args.generate_type + .map(|t| match t { + EnqueueHunyuan3dV3ImageTo3dGenerateType::Normal => "Normal", + EnqueueHunyuan3dV3ImageTo3dGenerateType::LowPoly => "LowPoly", + EnqueueHunyuan3dV3ImageTo3dGenerateType::Geometry => "Geometry", + }) + .map(|s| s.to_string()); + + let polygon_type = args.polygon_type + .map(|t| match t { + EnqueueHunyuan3dV3ImageTo3dPolygonType::Triangle => "triangle", + EnqueueHunyuan3dV3ImageTo3dPolygonType::Quadrilateral => "quadrilateral", + }) + .map(|s| s.to_string()); + + let request = Hunyuan3dV3ImageTo3dInput { + input_image_url: args.image_url, + // Optionals + back_image_url: args.back_image_url, + left_image_url: args.left_image_url, + right_image_url: args.right_image_url, + face_count: args.face_count, + generate_type, + polygon_type, + enable_pbr: args.enable_pbr, + }; + + let result = hunyuan3d_v3_image_to_3d(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 crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::object::enqueue_hunyuan3d_v3_image_to_3d_webhook::{enqueue_hunyuan3d_v3_image_to_3d_webhook, EnqueueHunyuan3dV3ImageTo3dArgs}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueHunyuan3dV3ImageTo3dArgs { + image_url: ERNEST_SCARED_STUPID_IMAGE_URL.to_string(), + back_image_url: None, + left_image_url: None, + right_image_url: None, + face_count: None, + generate_type: None, + polygon_type: None, + api_key: &api_key, + webhook_url: "https://example.com/webhook", + enable_pbr: None, + }; + + let result = enqueue_hunyuan3d_v3_image_to_3d_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/object/enqueue_hunyuan3d_v3_sketch_to_3d_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/object/enqueue_hunyuan3d_v3_sketch_to_3d_webhook.rs new file mode 100644 index 0000000000..cef1b67bf4 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/object/enqueue_hunyuan3d_v3_sketch_to_3d_webhook.rs @@ -0,0 +1,133 @@ +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::hunyuan3d::hunyuan3d_v3_sketch_to_3d::{hunyuan3d_v3_sketch_to_3d, Hunyuan3dV3SketchTo3dInput}; + +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueHunyuan3dV3SketchTo3dArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + pub image_url: String, + + // Optional args + pub face_count: Option, + pub generate_type: Option, + pub polygon_type: Option, + pub enable_pbr: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueHunyuan3dV3SketchTo3dGenerateType { + Normal, + LowPoly, + Geometry, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueHunyuan3dV3SketchTo3dPolygonType { + Triangle, + Quadrilateral, +} + +impl FalRequestCostCalculator for EnqueueHunyuan3dV3SketchTo3dArgs<'_, R> { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Generation types: Normal costs $0.375, + // LowPoly costs $0.45, + // Geometry costs $0.225. + // Enabling PBR materials adds $0.15. + // Custom face count adds $0.15. + let mut cost = match self.generate_type { + None => 38, // Round up from $0.375 + Some(EnqueueHunyuan3dV3SketchTo3dGenerateType::Normal) => 38, // Round up + Some(EnqueueHunyuan3dV3SketchTo3dGenerateType::Geometry) => 23, // Round up + Some(EnqueueHunyuan3dV3SketchTo3dGenerateType::LowPoly) => 45, + }; + if self.enable_pbr.unwrap_or(false) { + cost += 15; + } + if self.face_count.is_some() { + cost += 15; + } + cost + } +} + + +/// Hunyuan3D V3 Sketch-to-3D +/// https://fal.ai/models/fal-ai/hunyuan3d-v3/sketch-to-3d +pub async fn enqueue_hunyuan3d_v3_sketch_to_3d_webhook( + args: EnqueueHunyuan3dV3SketchTo3dArgs<'_, R> +) -> Result { + + let generate_type = args.generate_type + .map(|t| match t { + EnqueueHunyuan3dV3SketchTo3dGenerateType::Normal => "Normal", + EnqueueHunyuan3dV3SketchTo3dGenerateType::LowPoly => "LowPoly", + EnqueueHunyuan3dV3SketchTo3dGenerateType::Geometry => "Geometry", + }) + .map(|s| s.to_string()); + + let polygon_type = args.polygon_type + .map(|t| match t { + EnqueueHunyuan3dV3SketchTo3dPolygonType::Triangle => "triangle", + EnqueueHunyuan3dV3SketchTo3dPolygonType::Quadrilateral => "quadrilateral", + }) + .map(|s| s.to_string()); + + let request = Hunyuan3dV3SketchTo3dInput { + prompt: args.prompt, + input_image_url: args.image_url, + // Optionals + face_count: args.face_count, + generate_type, + polygon_type, + enable_pbr: args.enable_pbr, + }; + + let result = hunyuan3d_v3_sketch_to_3d(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 crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::object::enqueue_hunyuan3d_v3_sketch_to_3d_webhook::{enqueue_hunyuan3d_v3_sketch_to_3d_webhook, EnqueueHunyuan3dV3SketchTo3dArgs}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueHunyuan3dV3SketchTo3dArgs { + prompt: "A cute robot".to_string(), + image_url: ERNEST_SCARED_STUPID_IMAGE_URL.to_string(), + face_count: None, + generate_type: None, + polygon_type: None, + api_key: &api_key, + webhook_url: "https://example.com/webhook", + enable_pbr: None, + }; + + let result = enqueue_hunyuan3d_v3_sketch_to_3d_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/object/enqueue_hunyuan3d_v3_text_to_3d_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/object/enqueue_hunyuan3d_v3_text_to_3d_webhook.rs new file mode 100644 index 0000000000..bae2a01233 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/object/enqueue_hunyuan3d_v3_text_to_3d_webhook.rs @@ -0,0 +1,129 @@ +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::hunyuan3d::hunyuan3d_v3_text_to_3d::{hunyuan3d_v3_text_to_3d, Hunyuan3dV3TextTo3dInput}; + +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueHunyuan3dV3TextTo3dArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Optional args + pub face_count: Option, + pub generate_type: Option, + pub polygon_type: Option, + pub enable_pbr: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueHunyuan3dV3TextTo3dGenerateType { + Normal, + LowPoly, + Geometry, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueHunyuan3dV3TextTo3dPolygonType { + Triangle, + Quadrilateral, +} + +impl FalRequestCostCalculator for EnqueueHunyuan3dV3TextTo3dArgs<'_, R> { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Generation types: Normal costs $0.375, + // LowPoly costs $0.45, + // Geometry costs $0.225. + // Enabling PBR materials adds $0.15. + // Custom face count adds $0.15. + let mut cost = match self.generate_type { + None => 38, // Round up from $0.375 + Some(EnqueueHunyuan3dV3TextTo3dGenerateType::Normal) => 38, // Round up + Some(EnqueueHunyuan3dV3TextTo3dGenerateType::Geometry) => 23, // Round up + Some(EnqueueHunyuan3dV3TextTo3dGenerateType::LowPoly) => 45, + }; + if self.enable_pbr.unwrap_or(false) { + cost += 15; + } + if self.face_count.is_some() { + cost += 15; + } + cost + } +} + + +/// Hunyuan3D V3 Text-to-3D +/// https://fal.ai/models/fal-ai/hunyuan3d-v3/text-to-3d +pub async fn enqueue_hunyuan3d_v3_text_to_3d_webhook( + args: EnqueueHunyuan3dV3TextTo3dArgs<'_, R> +) -> Result { + + let generate_type = args.generate_type + .map(|t| match t { + EnqueueHunyuan3dV3TextTo3dGenerateType::Normal => "Normal", + EnqueueHunyuan3dV3TextTo3dGenerateType::LowPoly => "LowPoly", + EnqueueHunyuan3dV3TextTo3dGenerateType::Geometry => "Geometry", + }) + .map(|s| s.to_string()); + + let polygon_type = args.polygon_type + .map(|t| match t { + EnqueueHunyuan3dV3TextTo3dPolygonType::Triangle => "triangle", + EnqueueHunyuan3dV3TextTo3dPolygonType::Quadrilateral => "quadrilateral", + }) + .map(|s| s.to_string()); + + let request = Hunyuan3dV3TextTo3dInput { + prompt: args.prompt, + // Optionals + face_count: args.face_count, + generate_type, + polygon_type, + enable_pbr: args.enable_pbr, + }; + + let result = hunyuan3d_v3_text_to_3d(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 crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::object::enqueue_hunyuan3d_v3_text_to_3d_webhook::{enqueue_hunyuan3d_v3_text_to_3d_webhook, EnqueueHunyuan3dV3TextTo3dArgs}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueHunyuan3dV3TextTo3dArgs { + prompt: "A velociraptor with an open mouth full of sharp teeth. Large claws, ready to strike.".to_string(), + face_count: None, + generate_type: None, + polygon_type: None, + api_key: &api_key, + webhook_url: "https://example.com/webhook", + enable_pbr: None, + }; + + let result = enqueue_hunyuan3d_v3_text_to_3d_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/object/mod.rs b/crates/api_clients/fal_client/src/requests/webhook/object/mod.rs index c818cc0721..c1c8ed493b 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/object/mod.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/object/mod.rs @@ -1,2 +1,5 @@ pub mod enqueue_hunyuan_3d_2_image_to_3d_webhook; -pub mod enqueue_hunyuan_3d_21_image_to_3d_webhook; \ No newline at end of file +pub mod enqueue_hunyuan_3d_21_image_to_3d_webhook; +pub mod enqueue_hunyuan3d_v3_image_to_3d_webhook; +pub mod enqueue_hunyuan3d_v3_sketch_to_3d_webhook; +pub mod enqueue_hunyuan3d_v3_text_to_3d_webhook; \ No newline at end of file diff --git a/crates/schema/public/enums/src/common/model_type.rs b/crates/schema/public/enums/src/common/model_type.rs index 648f6ec0c0..a2809097e3 100644 --- a/crates/schema/public/enums/src/common/model_type.rs +++ b/crates/schema/public/enums/src/common/model_type.rs @@ -113,6 +113,8 @@ pub enum ModelType { Hunyuan3d2_0, #[serde(rename = "hunyuan_3d_2p1")] Hunyuan3d2_1, + #[serde(rename = "hunyuan_3d_3")] + Hunyuan3d3, } impl_enum_display_and_debug_using_to_str!(ModelType); @@ -172,6 +174,7 @@ impl ModelType { // 3D Object generation models Self::Hunyuan3d2_0 => "hunyuan_3d_2p0", Self::Hunyuan3d2_1 => "hunyuan_3d_2p1", + Self::Hunyuan3d3 => "hunyuan_3d_3", } } @@ -225,6 +228,7 @@ impl ModelType { // 3D Object generation models "hunyuan_3d_2p0" => Ok(Self::Hunyuan3d2_0), "hunyuan_3d_2p1" => Ok(Self::Hunyuan3d2_1), + "hunyuan_3d_3" => Ok(Self::Hunyuan3d3), _ => Err(format!("invalid model_type: {:?}", job_status)), } @@ -282,6 +286,7 @@ impl ModelType { // 3D Object generation models Self::Hunyuan3d2_0, Self::Hunyuan3d2_1, + Self::Hunyuan3d3, ]) } } @@ -342,6 +347,7 @@ mod tests { // 3D Object generation models assert_serialization(ModelType::Hunyuan3d2_0, "hunyuan_3d_2p0"); assert_serialization(ModelType::Hunyuan3d2_1, "hunyuan_3d_2p1"); + assert_serialization(ModelType::Hunyuan3d3, "hunyuan_3d_3"); } #[test] @@ -394,6 +400,7 @@ mod tests { // 3D Object generation models assert_eq!(ModelType::Hunyuan3d2_0.to_str(), "hunyuan_3d_2p0"); assert_eq!(ModelType::Hunyuan3d2_1.to_str(), "hunyuan_3d_2p1"); + assert_eq!(ModelType::Hunyuan3d3.to_str(), "hunyuan_3d_3"); } #[test] @@ -445,12 +452,13 @@ mod tests { // 3D Object generation models assert_eq!(ModelType::from_str("hunyuan_3d_2p0").unwrap(), ModelType::Hunyuan3d2_0); assert_eq!(ModelType::from_str("hunyuan_3d_2p1").unwrap(), ModelType::Hunyuan3d2_1); + assert_eq!(ModelType::from_str("hunyuan_3d_3").unwrap(), ModelType::Hunyuan3d3); } #[test] fn all_variants() { let mut variants = ModelType::all_variants(); - assert_eq!(variants.len(), 43); + assert_eq!(variants.len(), 44); // Image models assert_eq!(variants.pop_first(), Some(ModelType::Flux1Dev)); assert_eq!(variants.pop_first(), Some(ModelType::Flux1Schnell)); @@ -497,6 +505,7 @@ mod tests { // 3D Object generation models assert_eq!(variants.pop_first(), Some(ModelType::Hunyuan3d2_0)); assert_eq!(variants.pop_first(), Some(ModelType::Hunyuan3d2_1)); + assert_eq!(variants.pop_first(), Some(ModelType::Hunyuan3d3)); assert_eq!(variants.pop_first(), None); } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/mod.rs index e057f8225e..d44717aafc 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/mod.rs @@ -1,2 +1,3 @@ +pub mod generate_hunyuan_2_0_image_to_3d_handler; pub mod generate_hunyuan_2_1_image_to_3d_handler; -pub mod generate_hunyuan_2_0_image_to_3d_handler; \ No newline at end of file +pub mod multi_function; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/multi_function/generate_hunyuan3d_v3_multi_function_object_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/multi_function/generate_hunyuan3d_v3_multi_function_object_handler.rs new file mode 100644 index 0000000000..5df73553d6 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/multi_function/generate_hunyuan3d_v3_multi_function_object_handler.rs @@ -0,0 +1,400 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test; +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::object::multi_function::hunyuan3d_v3_multi_function_object_gen::{ + Hunyuan3dV3GenerateType, Hunyuan3dV3MultiFunctionObjectGenRequest, + Hunyuan3dV3MultiFunctionObjectGenResponse, Hunyuan3dV3PolygonType, +}; +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::requests::webhook::object::enqueue_hunyuan3d_v3_image_to_3d_webhook::{ + enqueue_hunyuan3d_v3_image_to_3d_webhook, EnqueueHunyuan3dV3ImageTo3dArgs, + EnqueueHunyuan3dV3ImageTo3dGenerateType, EnqueueHunyuan3dV3ImageTo3dPolygonType, +}; +use fal_client::requests::webhook::object::enqueue_hunyuan3d_v3_sketch_to_3d_webhook::{ + enqueue_hunyuan3d_v3_sketch_to_3d_webhook, EnqueueHunyuan3dV3SketchTo3dArgs, + EnqueueHunyuan3dV3SketchTo3dGenerateType, EnqueueHunyuan3dV3SketchTo3dPolygonType, +}; +use fal_client::requests::webhook::object::enqueue_hunyuan3d_v3_text_to_3d_webhook::{ + enqueue_hunyuan3d_v3_text_to_3d_webhook, EnqueueHunyuan3dV3TextTo3dArgs, + EnqueueHunyuan3dV3TextTo3dGenerateType, EnqueueHunyuan3dV3TextTo3dPolygonType, +}; +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::{ + insert_generic_inference_job_for_fal_queue, FalCategory, InsertGenericInferenceForFalArgs, +}; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +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 sqlx::Acquire; +use tokens::tokens::media_files::MediaFileToken; + +/// Hunyuan 3D v3 Multi-Function (text-to-3d, image-to-3d, sketch-to-3d) +#[utoipa::path( + post, + tag = "Generate Objects (Multi-Function)", + path = "/v1/generate/object/multi_function/hunyuan3d_v3", + responses( + (status = 200, description = "Success", body = Hunyuan3dV3MultiFunctionObjectGenResponse), + ), + params( + ("request" = Hunyuan3dV3MultiFunctionObjectGenRequest, description = "Payload for Request"), + ) +)] +pub async fn generate_hunyuan3d_v3_multi_function_object_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); + + if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) { + return Err(CommonWebError::BadInputWithSimpleMessage(reason)); + } + + // Collect all media tokens to look up in a single batch query + let mut query_media_tokens: Vec = Vec::new(); + + if let Some(token) = &request.image_media_token { + query_media_tokens.push(token.clone()); + } + if let Some(token) = &request.back_image_media_token { + query_media_tokens.push(token.clone()); + } + if let Some(token) = &request.left_image_media_token { + query_media_tokens.push(token.clone()); + } + if let Some(token) = &request.right_image_media_token { + query_media_tokens.push(token.clone()); + } + + // Perform a single batch lookup for all image URLs + let image_urls_by_token = if query_media_tokens.is_empty() { + HashMap::new() + } else { + info!("Looking up image media tokens: {:?}", query_media_tokens); + lookup_image_urls_as_map( + &http_request, + &mut mysql_connection, + server_state.server_environment, + &query_media_tokens, + ) + .await? + }; + + // Extract individual URLs from the map + let image_url = request + .image_media_token + .as_ref() + .and_then(|token| image_urls_by_token.get(token)) + .map(|url| url.to_string()); + + let back_image_url = request + .back_image_media_token + .as_ref() + .and_then(|token| image_urls_by_token.get(token)) + .map(|url| url.to_string()); + + let left_image_url = request + .left_image_media_token + .as_ref() + .and_then(|token| image_urls_by_token.get(token)) + .map(|url| url.to_string()); + + let right_image_url = request + .right_image_media_token + .as_ref() + .and_then(|token| image_urls_by_token.get(token)) + .map(|url| url.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); + + // Determine which mode we're in based on inputs + let has_prompt = request + .prompt + .as_ref() + .map(|p| !p.trim().is_empty()) + .unwrap_or(false); + + let has_image = image_url.is_some(); + + let fal_result = match (has_prompt, has_image) { + // Text-to-3D: prompt only, no image + (true, false) => { + info!("hunyuan3d v3 text-to-3d mode"); + + let generate_type = request.generate_type.map(|t| match t { + Hunyuan3dV3GenerateType::Normal => EnqueueHunyuan3dV3TextTo3dGenerateType::Normal, + Hunyuan3dV3GenerateType::LowPoly => EnqueueHunyuan3dV3TextTo3dGenerateType::LowPoly, + Hunyuan3dV3GenerateType::Geometry => EnqueueHunyuan3dV3TextTo3dGenerateType::Geometry, + }); + + let polygon_type = request.polygon_type.map(|t| match t { + Hunyuan3dV3PolygonType::Triangle => EnqueueHunyuan3dV3TextTo3dPolygonType::Triangle, + Hunyuan3dV3PolygonType::Quadrilateral => { + EnqueueHunyuan3dV3TextTo3dPolygonType::Quadrilateral + } + }); + + let args = EnqueueHunyuan3dV3TextTo3dArgs { + prompt: request.prompt.clone().unwrap_or_default(), + face_count: request.face_count, + generate_type, + polygon_type, + enable_pbr: request.enable_pbr, + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + enqueue_hunyuan3d_v3_text_to_3d_webhook(args) + .await + .map_err(|err| { + warn!( + "Error calling enqueue_hunyuan3d_v3_text_to_3d_webhook: {:?}", + err + ); + CommonWebError::ServerError + })? + } + + // Sketch-to-3D: both prompt and image + (true, true) => { + info!("hunyuan3d v3 sketch-to-3d mode"); + + let generate_type = request.generate_type.map(|t| match t { + Hunyuan3dV3GenerateType::Normal => EnqueueHunyuan3dV3SketchTo3dGenerateType::Normal, + Hunyuan3dV3GenerateType::LowPoly => EnqueueHunyuan3dV3SketchTo3dGenerateType::LowPoly, + Hunyuan3dV3GenerateType::Geometry => EnqueueHunyuan3dV3SketchTo3dGenerateType::Geometry, + }); + + let polygon_type = request.polygon_type.map(|t| match t { + Hunyuan3dV3PolygonType::Triangle => EnqueueHunyuan3dV3SketchTo3dPolygonType::Triangle, + Hunyuan3dV3PolygonType::Quadrilateral => { + EnqueueHunyuan3dV3SketchTo3dPolygonType::Quadrilateral + } + }); + + let args = EnqueueHunyuan3dV3SketchTo3dArgs { + prompt: request.prompt.clone().unwrap_or_default(), + image_url: image_url.unwrap(), + face_count: request.face_count, + generate_type, + polygon_type, + enable_pbr: request.enable_pbr, + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + enqueue_hunyuan3d_v3_sketch_to_3d_webhook(args) + .await + .map_err(|err| { + warn!( + "Error calling enqueue_hunyuan3d_v3_sketch_to_3d_webhook: {:?}", + err + ); + CommonWebError::ServerError + })? + } + + // Image-to-3D: image only, no prompt (or empty prompt) + (false, true) => { + info!("hunyuan3d v3 image-to-3d mode"); + + let generate_type = request.generate_type.map(|t| match t { + Hunyuan3dV3GenerateType::Normal => EnqueueHunyuan3dV3ImageTo3dGenerateType::Normal, + Hunyuan3dV3GenerateType::LowPoly => EnqueueHunyuan3dV3ImageTo3dGenerateType::LowPoly, + Hunyuan3dV3GenerateType::Geometry => EnqueueHunyuan3dV3ImageTo3dGenerateType::Geometry, + }); + + let polygon_type = request.polygon_type.map(|t| match t { + Hunyuan3dV3PolygonType::Triangle => EnqueueHunyuan3dV3ImageTo3dPolygonType::Triangle, + Hunyuan3dV3PolygonType::Quadrilateral => { + EnqueueHunyuan3dV3ImageTo3dPolygonType::Quadrilateral + } + }); + + let args = EnqueueHunyuan3dV3ImageTo3dArgs { + image_url: image_url.unwrap(), + back_image_url, + left_image_url, + right_image_url, + face_count: request.face_count, + generate_type, + polygon_type, + enable_pbr: request.enable_pbr, + webhook_url: &server_state.fal.webhook_url, + api_key: &server_state.fal.api_key, + }; + + enqueue_hunyuan3d_v3_image_to_3d_webhook(args) + .await + .map_err(|err| { + warn!( + "Error calling enqueue_hunyuan3d_v3_image_to_3d_webhook: {:?}", + err + ); + CommonWebError::ServerError + })? + } + + // Neither prompt nor image - invalid request + (false, false) => { + return Err(CommonWebError::BadInputWithSimpleMessage( + "Either prompt or image_media_token must be provided".to_string(), + )); + } + }; + + 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 + })?; + + // Insert prompt record if we have a prompt + 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::Hunyuan3d3), + 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. + } + }; + + // Insert context items for any images used + if let Some(token) = prompt_token.as_ref() { + let mut context_items = Vec::new(); + + if let Some(media_token) = &request.image_media_token { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::Imgref, + }); + } + if let Some(media_token) = &request.back_image_media_token { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::Imgref, + }); + } + if let Some(media_token) = &request.left_image_media_token { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::Imgref, + }); + } + if let Some(media_token) = &request.right_image_media_token { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::Imgref, + }); + } + + 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(InsertGenericInferenceForFalArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + maybe_external_third_party_id: &external_job_id, + fal_category: FalCategory::ObjectGeneration, + 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, + 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(Hunyuan3dV3MultiFunctionObjectGenResponse { + success: true, + inference_job_token: job_token, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/multi_function/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/multi_function/mod.rs new file mode 100644 index 0000000000..6da6989b85 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/object/multi_function/mod.rs @@ -0,0 +1 @@ +pub mod generate_hunyuan3d_v3_multi_function_object_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 b8635771fd..0e2e2d189f 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 @@ -18,6 +18,7 @@ use crate::http_server::endpoints::generate::image::multi_function::nano_banana_ use crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler; use crate::http_server::endpoints::generate::object::generate_hunyuan_2_0_image_to_3d_handler::generate_hunyuan_2_0_image_to_3d_handler; use crate::http_server::endpoints::generate::object::generate_hunyuan_2_1_image_to_3d_handler::generate_hunyuan_2_1_image_to_3d_handler; +use crate::http_server::endpoints::generate::object::multi_function::generate_hunyuan3d_v3_multi_function_object_handler::generate_hunyuan3d_v3_multi_function_object_handler; use crate::http_server::endpoints::generate::video::generate_kling_1_6_pro_video_handler::generate_kling_1_6_pro_video_handler; use crate::http_server::endpoints::generate::video::generate_kling_2_1_master_video_handler::generate_kling_2_1_master_video_handler; use crate::http_server::endpoints::generate::video::generate_kling_2_1_pro_video_handler::generate_kling_2_1_pro_video_handler; @@ -190,6 +191,12 @@ where ) ) .service(web::scope("/object") + .service(web::scope("/multi_function") + .service(web::resource("/hunyuan3d_v3") + .route(web::post().to(generate_hunyuan3d_v3_multi_function_object_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + ) .service(web::resource("/hunyuan_2.0_image_to_3d") .route(web::post().to(generate_hunyuan_2_0_image_to_3d_handler)) .route(web::head().to(|| HttpResponse::Ok())) diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/hunyuan3d_v3_image_to_3d.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/hunyuan3d_v3_image_to_3d.rs new file mode 100644 index 0000000000..ce9063a2fa --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/hunyuan3d_v3_image_to_3d.rs @@ -0,0 +1,63 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Hunyuan3dV3ImageTo3dInput { + /// REQUIRED + pub input_image_url: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub back_image_url : Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub left_image_url : Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub right_image_url : Option, + + /// Target face count. Range: 40000-1500000 Default value: 500000 + #[serde(skip_serializing_if = "Option::is_none")] + pub face_count: Option, + + /// Generation type. + /// Normal: textured model. + /// LowPoly: polygon reduction. + /// Geometry: white model without texture. + /// Default value: "Normal" + /// Possible enum values: Normal, LowPoly, Geometry + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_type: Option, + + /// Polygon type. Only takes effect when GenerateType is LowPoly. + /// Default value: "triangle" + /// Possible enum values: triangle, quadrilateral + #[serde(skip_serializing_if = "Option::is_none")] + pub polygon_type: Option, + + /// Whether to enable PBR material generation. + /// Does not take effect when generate_type is Geometry. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_pbr: Option, + +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Hunyuan3dV3ImageTo3dOutput { + pub model_glb: Hunyuan3dV3ImageTo3dModelGlb, + pub thumbnail: Hunyuan3dV3ImageTo3dThumbnail, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Hunyuan3dV3ImageTo3dModelGlb { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Hunyuan3dV3ImageTo3dThumbnail { + pub url: String, +} + +pub fn hunyuan3d_v3_image_to_3d( + params: Hunyuan3dV3ImageTo3dInput, +) -> FalRequest { + FalRequest::new("fal-ai/hunyuan3d-v3/image-to-3d", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/hunyuan3d_v3_sketch_to_3d.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/hunyuan3d_v3_sketch_to_3d.rs new file mode 100644 index 0000000000..4c4349f013 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/hunyuan3d_v3_sketch_to_3d.rs @@ -0,0 +1,46 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Hunyuan3dV3SketchTo3dInput { + /// REQUIRED + pub prompt: String, + + /// REQUIRED + pub input_image_url: String, + + /// Target face count. Range: 40000-1500000 Default value: 500000 + #[serde(skip_serializing_if = "Option::is_none")] + pub face_count: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_type: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub polygon_type: Option, + + /// Whether to enable PBR material generation. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_pbr: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Hunyuan3dV3SketchTo3dOutput { + pub model_glb: Hunyuan3dV3SketchTo3dModelGlb, + pub thumbnail: Hunyuan3dV3SketchTo3dThumbnail, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Hunyuan3dV3SketchTo3dModelGlb { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Hunyuan3dV3SketchTo3dThumbnail { + pub url: String, +} + +pub fn hunyuan3d_v3_sketch_to_3d( + params: Hunyuan3dV3SketchTo3dInput, +) -> FalRequest { + FalRequest::new("fal-ai/hunyuan3d-v3/sketch-to-3d", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/hunyuan3d_v3_text_to_3d.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/hunyuan3d_v3_text_to_3d.rs new file mode 100644 index 0000000000..f4a49dad06 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/hunyuan3d_v3_text_to_3d.rs @@ -0,0 +1,43 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Hunyuan3dV3TextTo3dInput { + /// REQUIRED + pub prompt: String, + + /// Target face count. Range: 40000-1500000 Default value: 500000 + #[serde(skip_serializing_if = "Option::is_none")] + pub face_count: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_type: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub polygon_type: Option, + + /// Whether to enable PBR material generation. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_pbr: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Hunyuan3dV3TextTo3dOutput { + pub model_glb: Hunyuan3dV3TextTo3dModelGlb, + pub thumbnail: Hunyuan3dV3TextTo3dThumbnail, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Hunyuan3dV3TextTo3dModelGlb { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Hunyuan3dV3TextTo3dThumbnail { + pub url: String, +} + +pub fn hunyuan3d_v3_text_to_3d( + params: Hunyuan3dV3TextTo3dInput, +) -> FalRequest { + FalRequest::new("fal-ai/hunyuan3d-v3/text-to-3d", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/mod.rs index 0d46dc31f0..0c84e23020 100644 --- a/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/mod.rs +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/hunyuan3d/mod.rs @@ -26,3 +26,45 @@ pub mod v2; ))) )] pub mod v21; +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_hunyuan3d" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_hunyuan3d" + ))) +)] +pub mod hunyuan3d_v3_image_to_3d; +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_hunyuan3d" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_hunyuan3d" + ))) +)] +pub mod hunyuan3d_v3_sketch_to_3d; +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_hunyuan3d" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_hunyuan3d" + ))) +)] +pub mod hunyuan3d_v3_text_to_3d;