Compare commits

...
Author SHA1 Message Date
Brandon Thomas 2646c1035e hunyuan2 img-to-3d upload 2025-06-05 00:13:46 -04:00
Brandon Thomas 01c2740b4c hunyuan3d 2025-06-04 23:07:08 -04:00
Brandon Thomas 8109084a46 hunyuan2 image-to-3d 2025-06-04 23:00:26 -04:00
Brandon Thomas 1fff0e232e more fal endpoints wip 2025-06-04 22:29:31 -04:00
23 changed files with 446 additions and 9 deletions
@@ -1,2 +1,3 @@
pub mod image;
pub mod object;
pub mod video;
@@ -0,0 +1,42 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::hunyuan3d::v2::{v2, Hunyuan3DInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Hunyuan2Args<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling16Duration {
Default,
FiveSeconds,
TenSeconds,
}
pub async fn enqueue_hunyuan2_image_to_3d_webhook<U: IntoUrl, V: IntoUrl>(
args: Hunyuan2Args<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let image_url = args.image_url.as_str().to_string();
let request = Hunyuan3DInput {
input_image_url: image_url,
textured_mesh: Some(true),
guidance_scale: None,
num_inference_steps: None,
octree_resolution: None,
seed: None,
};
let result = v2(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
@@ -0,0 +1 @@
pub mod enqueue_hunyuan2_image_to_3d_webhook;
@@ -175,6 +175,7 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
InferenceCategory::VideoFilter => Some("Video Filter"),
InferenceCategory::ImageGeneration => Some("Image Generation"),
InferenceCategory::VideoGeneration => Some("Video Generation"),
InferenceCategory::ObjectGeneration => Some("Object Generation"),
InferenceCategory::BackgroundRemoval => Some("Background Removal"),
InferenceCategory::Mocap => Some("Mocap"),
InferenceCategory::Workflow => Some("Workflow"),
@@ -197,6 +198,7 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
InferenceCategory::VideoFilter => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::ImageGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::VideoGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::ObjectGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::BackgroundRemoval => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::Mocap => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::Workflow => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
@@ -151,6 +151,7 @@ fn raw_record_to_public_result(record: RawGenericInferenceJobStatus) -> GenericI
InferenceCategory::VideoFilter => Some("Video Filter"),
InferenceCategory::ImageGeneration => Some("Image Generation"),
InferenceCategory::VideoGeneration => Some("Video Generation"),
InferenceCategory::ObjectGeneration => Some("Object Generation"),
InferenceCategory::BackgroundRemoval => Some("Background Removal"),
InferenceCategory::Mocap => Some("Mocap"),
InferenceCategory::Workflow => Some("Workflow"),
@@ -173,6 +174,7 @@ fn raw_record_to_public_result(record: RawGenericInferenceJobStatus) -> GenericI
InferenceCategory::VideoFilter => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::ImageGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::VideoGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::ObjectGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::BackgroundRemoval => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::Mocap => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::Workflow => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
@@ -24,6 +24,7 @@ pub enum FalCategory {
ImageGeneration,
VideoGeneration,
BackgroundRemoval,
ObjectGeneration,
}
pub struct InsertGenericInferenceForFalArgs<'a> {
@@ -69,6 +70,10 @@ pub async fn insert_generic_inference_job_for_fal_queue(args: InsertGenericInfer
InferenceCategory::BackgroundRemoval,
InferenceJobProductCategory::FalBgRemoval
),
FalCategory::ObjectGeneration => (
InferenceCategory::ObjectGeneration,
InferenceJobProductCategory::FalObject,
),
};
@@ -242,6 +242,7 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
InferenceCategory::VideoFilter => Some("Video Filter"),
InferenceCategory::ImageGeneration => Some("Image Generation"),
InferenceCategory::VideoGeneration => Some("Video Generation"),
InferenceCategory::ObjectGeneration => Some("Object Generation"),
InferenceCategory::BackgroundRemoval => Some("Background Removal"),
InferenceCategory::Mocap => Some("Mocap"),
InferenceCategory::Workflow => Some("Workflow"),
@@ -263,6 +264,7 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
InferenceCategory::VideoFilter => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::ImageGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::VideoGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::ObjectGeneration => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::BackgroundRemoval => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::Mocap => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::Workflow => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
@@ -1,2 +1,3 @@
pub mod image;
pub mod object;
pub mod video;
@@ -0,0 +1,25 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
const GENERATE_HUNYUAN_2_IMAGE_TO_3D_URL_PATH: &str = "/v1/generate/object/hunyuan_2_image_to_3d";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateHunyuan2ImageTo3dRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Source of the file to convert to 3D
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateHunyuan2ImageTo3dResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1 @@
pub mod generate_hunyuan_2_image_to_3d;
@@ -10,7 +10,7 @@ pub struct GenerateKling16ImageToVideoRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Source of the file to remove the background from.
/// Source of the image file to convert to video.
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
@@ -43,6 +43,10 @@ pub enum InferenceCategory {
#[serde(rename = "video_generation")]
VideoGeneration,
/// FAL-powered 3D object generation
#[serde(rename = "object_generation")]
ObjectGeneration,
/// FAL-powered image background removal
#[serde(rename = "background_removal")]
BackgroundRemoval,
@@ -98,6 +102,7 @@ impl InferenceCategory {
Self::VoiceConversion => "voice_conversion",
Self::ImageGeneration => "image_generation",
Self::VideoGeneration => "video_generation",
Self::ObjectGeneration => "object_generation",
Self::BackgroundRemoval => "background_removal",
Self::Mocap => "mocap",
Self::Workflow => "workflow",
@@ -118,6 +123,7 @@ impl InferenceCategory {
"voice_conversion" => Ok(Self::VoiceConversion),
"image_generation" => Ok(Self::ImageGeneration),
"video_generation" => Ok(Self::VideoGeneration),
"object_generation" => Ok(Self::ObjectGeneration),
"background_removal" => Ok(Self::BackgroundRemoval),
"f5_tts" => Ok(Self::F5TTS),
"mocap" => Ok(Self::Mocap),
@@ -140,6 +146,7 @@ impl InferenceCategory {
Self::TextToSpeech,
Self::VoiceConversion,
Self::ImageGeneration,
Self::ObjectGeneration,
Self::VideoGeneration,
Self::BackgroundRemoval,
Self::Mocap,
@@ -170,6 +177,7 @@ mod tests {
assert_serialization(InferenceCategory::VoiceConversion, "voice_conversion");
assert_serialization(InferenceCategory::ImageGeneration, "image_generation");
assert_serialization(InferenceCategory::VideoGeneration, "video_generation");
assert_serialization(InferenceCategory::ObjectGeneration, "object_generation");
assert_serialization(InferenceCategory::BackgroundRemoval, "background_removal");
assert_serialization(InferenceCategory::Mocap, "mocap");
assert_serialization(InferenceCategory::F5TTS, "f5_tts");
@@ -189,6 +197,7 @@ mod tests {
assert_eq!(InferenceCategory::VoiceConversion.to_str(), "voice_conversion");
assert_eq!(InferenceCategory::ImageGeneration.to_str(), "image_generation");
assert_eq!(InferenceCategory::VideoGeneration.to_str(), "video_generation");
assert_eq!(InferenceCategory::ObjectGeneration.to_str(), "object_generation");
assert_eq!(InferenceCategory::BackgroundRemoval.to_str(), "background_removal");
assert_eq!(InferenceCategory::F5TTS.to_str(), "f5_tts");
assert_eq!(InferenceCategory::SeedVc.to_str(), "seed_vc");
@@ -208,6 +217,7 @@ mod tests {
assert_eq!(InferenceCategory::from_str("voice_conversion").unwrap(), InferenceCategory::VoiceConversion);
assert_eq!(InferenceCategory::from_str("image_generation").unwrap(), InferenceCategory::ImageGeneration);
assert_eq!(InferenceCategory::from_str("video_generation").unwrap(), InferenceCategory::VideoGeneration);
assert_eq!(InferenceCategory::from_str("object_generation").unwrap(), InferenceCategory::ObjectGeneration);
assert_eq!(InferenceCategory::from_str("background_removal").unwrap(), InferenceCategory::BackgroundRemoval);
assert_eq!(InferenceCategory::from_str("f5_tts").unwrap(), InferenceCategory::F5TTS);
assert_eq!(InferenceCategory::from_str("seed_vc").unwrap(), InferenceCategory::SeedVc);
@@ -222,7 +232,7 @@ mod tests {
#[test]
fn all_variants() {
// Static check
const EXPECTED_COUNT : usize = 15;
const EXPECTED_COUNT : usize = 16;
assert_eq!(InferenceCategory::all_variants().len(), EXPECTED_COUNT);
assert_eq!(InferenceCategory::iter().len(), EXPECTED_COUNT);
@@ -26,6 +26,8 @@ pub enum InferenceJobProductCategory {
FalImage,
FalVideo,
/// Fal: 3D Object Generation
FalObject,
FalBgRemoval,
// =============== TEXT TO SPEECH ===============
@@ -121,6 +123,7 @@ impl InferenceJobProductCategory {
Self::DownloadGptSoVits => "download_gpt_so_vits",
Self::FalImage => "fal_image",
Self::FalVideo => "fal_video",
Self::FalObject => "fal_object",
Self::FalBgRemoval => "fal_bg_removal",
Self::TtsGptSoVits => "tts_gpt_so_vits",
Self::TtsStyleTts2 => "tts_style_tts2",
@@ -152,6 +155,7 @@ impl InferenceJobProductCategory {
"download_gpt_so_vits" => Ok(Self::DownloadGptSoVits),
"fal_image" => Ok(Self::FalImage),
"fal_video" => Ok(Self::FalVideo),
"fal_object" => Ok(Self::FalObject),
"fal_bg_removal" => Ok(Self::FalBgRemoval),
"tts_gpt_so_vits" => Ok(Self::TtsGptSoVits),
"tts_style_tts2" => Ok(Self::TtsStyleTts2),
@@ -186,6 +190,7 @@ impl InferenceJobProductCategory {
Self::DownloadGptSoVits,
Self::FalImage,
Self::FalVideo,
Self::FalObject,
Self::FalBgRemoval,
Self::TtsGptSoVits,
Self::TtsStyleTts2,
@@ -226,6 +231,7 @@ mod tests {
assert_serialization(InferenceJobProductCategory::DownloadGptSoVits, "download_gpt_so_vits");
assert_serialization(InferenceJobProductCategory::FalImage, "fal_image");
assert_serialization(InferenceJobProductCategory::FalVideo, "fal_video");
assert_serialization(InferenceJobProductCategory::FalObject, "fal_object");
assert_serialization(InferenceJobProductCategory::FalBgRemoval, "fal_bg_removal");
assert_serialization(InferenceJobProductCategory::TtsGptSoVits, "tts_gpt_so_vits");
assert_serialization(InferenceJobProductCategory::TtsStyleTts2, "tts_style_tts2");
@@ -256,6 +262,7 @@ mod tests {
assert_eq!(InferenceJobProductCategory::DownloadGptSoVits.to_str(), "download_gpt_so_vits");
assert_eq!(InferenceJobProductCategory::FalImage.to_str(), "fal_image");
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::TtsGptSoVits.to_str(), "tts_gpt_so_vits");
assert_eq!(InferenceJobProductCategory::TtsStyleTts2.to_str(), "tts_style_tts2");
@@ -285,6 +292,7 @@ mod tests {
assert_eq!(InferenceJobProductCategory::from_str("download_gpt_so_vits").unwrap(), InferenceJobProductCategory::DownloadGptSoVits);
assert_eq!(InferenceJobProductCategory::from_str("fal_image").unwrap(), InferenceJobProductCategory::FalImage);
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("tts_gpt_so_vits").unwrap(), InferenceJobProductCategory::TtsGptSoVits);
assert_eq!(InferenceJobProductCategory::from_str("tts_style_tts2").unwrap(), InferenceJobProductCategory::TtsStyleTts2);
@@ -312,7 +320,7 @@ mod tests {
#[test]
fn all_variants() {
// Static check
const EXPECTED_COUNT : usize = 26;
const EXPECTED_COUNT : usize = 27;
assert_eq!(InferenceJobProductCategory::all_variants().len(), EXPECTED_COUNT);
assert_eq!(InferenceJobProductCategory::iter().len(), EXPECTED_COUNT);
@@ -112,10 +112,10 @@ impl MediaFileType {
"image/jpeg" => Some(Self::Jpg),
"image/png" => Some(Self::Png),
"video/mp4" => Some(Self::Mp4),
"model/gltf-binary" => Some(Self::Glb),
//"audio/wav" => Some(Self::Wav), // NB: Not sure if this is correct
//"audio/mpeg" => Some(Self::Mp3), // NB: Not sure if this is correct
//"image/gif" => Some(Self::Gif), // NB: Not sure if this is correct
//"model/gltf-binary" => Some(Self::Glb), // NB: Not sure if this is correct
//"model/gltf+json" => Some(Self::Gltf), // NB: Not sure if this is correct
_ => None,
}
@@ -360,7 +360,9 @@ async fn old_dispatch(
return Err(ProcessSingleJobError::InvalidJob(
anyhow!("invalid job category for dispatch: {:?}", job.inference_category)))
}
InferenceCategory::VideoGeneration | InferenceCategory::BackgroundRemoval => {
InferenceCategory::VideoGeneration
| InferenceCategory::BackgroundRemoval
| InferenceCategory::ObjectGeneration => {
// These are handled by the new dispatch
return Err(ProcessSingleJobError::InvalidJob(
anyhow!("these job types do not support the old-style dispatch: {:?}", job.inference_category)))
@@ -1,2 +1,3 @@
pub mod image;
pub mod object;
pub mod video;
@@ -0,0 +1,222 @@
use std::fmt;
use std::sync::Arc;
use crate::http_server::common_requests::media_file_token_path_info::MediaFileTokenPathInfo;
use crate::http_server::common_responses::media::media_links::MediaLinks;
use crate::http_server::deprecated_endpoints::engine::create_scene_handler::CreateSceneError;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
use crate::state::server_state::ServerState;
use crate::util::delete_role_disambiguation::{delete_role_disambiguation, DeleteRole};
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::web::Path;
use actix_web::{web, HttpRequest, HttpResponse};
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dRequest;
use artcraft_api_defs::generate::object::generate_hunyuan_2_image_to_3d::GenerateHunyuan2ImageTo3dResponse;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::common::visibility::Visibility;
use fal_client::requests::webhook::object::enqueue_hunyuan2_image_to_3d_webhook::enqueue_hunyuan2_image_to_3d_webhook;
use fal_client::requests::webhook::object::enqueue_hunyuan2_image_to_3d_webhook::Hunyuan2Args;
use http_server_common::request::get_request_ip::get_request_ip;
use http_server_common::response::serialize_as_json_error::serialize_as_json_error;
use idempotency::uuid::generate_random_uuid;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job_for_fal_queue::insert_generic_inference_job_for_fal_queue;
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job_for_fal_queue::InsertGenericInferenceForFalArgs;
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, MediaFile};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
// =============== Error Response ===============
#[derive(Debug, Serialize, ToSchema)]
pub enum GenerateHunyuan2ImageTo3dError {
BadInput(String),
NotFound,
NotAuthorized,
ServerError,
}
impl ResponseError for GenerateHunyuan2ImageTo3dError {
fn status_code(&self) -> StatusCode {
match *self {
GenerateHunyuan2ImageTo3dError::BadInput(_) => StatusCode::BAD_REQUEST,
GenerateHunyuan2ImageTo3dError::NotFound => StatusCode::NOT_FOUND,
GenerateHunyuan2ImageTo3dError::NotAuthorized => StatusCode::UNAUTHORIZED,
GenerateHunyuan2ImageTo3dError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
serialize_as_json_error(self)
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for GenerateHunyuan2ImageTo3dError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
// =============== Handler ===============
/// Background removal
#[utoipa::path(
post,
tag = "Generate Objects",
path = "/v1/generate/object/hunyuan_2_image_to_3d",
responses(
(status = 200, description = "Success", body = GenerateHunyuan2ImageTo3dResponse),
(status = 400, description = "Bad input", body = GenerateHunyuan2ImageTo3dError),
(status = 401, description = "Not authorized", body = GenerateHunyuan2ImageTo3dError),
(status = 500, description = "Server error", body = GenerateHunyuan2ImageTo3dError),
),
params(
("request" = GenerateHunyuan2ImageTo3dRequest, description = "Payload for Request"),
)
)]
pub async fn generate_hunyuan_2_image_to_3d_handler(
http_request: HttpRequest,
request: Json<GenerateHunyuan2ImageTo3dRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<GenerateHunyuan2ImageTo3dResponse>, GenerateHunyuan2ImageTo3dError> {
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
GenerateHunyuan2ImageTo3dError::ServerError
})?;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// TODO: Limit usage for new accounts. Billing, free credits metering, etc.
//let user_session = match maybe_user_session {
// Some(session) => session,
// None => {
// warn!("not logged in");
// return Err(GenerateHunyuan2ImageTo3dError::NotAuthorized);
// }
//};
let media_file_token = match &request.media_file_token {
Some(token) => token,
None => {
warn!("No media file token provided");
return Err(GenerateHunyuan2ImageTo3dError::BadInput("No media file token provided".to_string()));
}
};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(GenerateHunyuan2ImageTo3dError::BadInput(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &server_state.mysql_pool)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
GenerateHunyuan2ImageTo3dError::BadInput("invalid idempotency token".to_string())
})?;
const IS_MOD : bool = false;
let media_file_lookup_result = get_media_file(
media_file_token,
IS_MOD,
&server_state.mysql_pool,
).await;
let media_file = match media_file_lookup_result {
Ok(Some(media_file)) => media_file,
Ok(None) => {
warn!("MediaFile not found: {:?}", media_file_token);
return Err(GenerateHunyuan2ImageTo3dError::NotFound);
},
Err(err) => {
warn!("Error looking up media_file: {:?}", err);
return Err(GenerateHunyuan2ImageTo3dError::ServerError);
}
};
if !media_file.media_type.is_jpg_or_png_or_legacy_image() {
return Err(GenerateHunyuan2ImageTo3dError::BadInput("Media file must be a JPG or PNG image".to_string()));
}
let media_domain = get_media_domain(&http_request);
let bucket_path = MediaFileBucketPath::from_object_hash(
&media_file.public_bucket_directory_hash,
media_file.maybe_public_bucket_prefix.as_deref(),
media_file.maybe_public_bucket_extension.as_deref());
let media_links = MediaLinks::from_media_path_and_env(
media_domain,
server_state.server_environment,
&bucket_path);
info!("Fal webhook URL: {}", server_state.fal.webhook_url);
let prompt = request.prompt
.as_deref()
.map(|prompt| prompt.trim())
.unwrap_or_else(|| "");
let args = Hunyuan2Args {
image_url: media_links.cdn_url,
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
let fal_result = enqueue_hunyuan2_image_to_3d_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_hunyuan2_image_to_3d_webhook: {:?}", err);
GenerateHunyuan2ImageTo3dError::ServerError
})?;
let external_job_id = fal_result.request_id
.ok_or_else(|| {
warn!("Fal request_id is None");
GenerateHunyuan2ImageTo3dError::ServerError
})?;
info!("Fal request_id: {}", external_job_id);
let ip_address = get_request_ip(&http_request);
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_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_pool: &server_state.mysql_pool,
}).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting generic inference job for FAL queue: {:?}", err);
return Err(GenerateHunyuan2ImageTo3dError::ServerError);
}
};
Ok(Json(GenerateHunyuan2ImageTo3dResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -0,0 +1 @@
pub mod generate_hunyuan2_image_to_3d_handler;
@@ -73,6 +73,7 @@ pub fn estimate_job_progress(job: &GenericInferenceJobStatus, maybe_args: Option
// TODO: Better estimates for FAL, etc.
InferenceCategory::VideoGeneration => percent(duration_seconds, 60*5),
InferenceCategory::BackgroundRemoval => percent(duration_seconds, 10),
InferenceCategory::ObjectGeneration => percent(duration_seconds, 60*2),
// TODO: Better estimate using video duration, params, etc.
InferenceCategory::Workflow => comfy_workflow_estimate(maybe_args, duration_seconds),
@@ -2,6 +2,7 @@ use std::fmt;
use std::sync::Arc;
use crate::http_server::endpoints::webhooks::handle_image_payload::handle_image_payload;
use crate::http_server::endpoints::webhooks::handle_model_mesh_payload::handle_model_mesh_payload;
use crate::http_server::endpoints::webhooks::handle_video_payload::handle_video_payload;
use crate::state::server_state::ServerState;
use actix_web::error::ResponseError;
@@ -144,9 +145,8 @@ pub async fn fal_webhook_handler(
handle_image_payload(payload_obj, &job, &server_state).await?;
} else if payload_obj.contains_key("video") {
handle_video_payload(payload_obj, &job, &server_state).await?;
}
if payload_obj.contains_key("video") {
handle_video_payload(payload_obj, &job, &server_state).await?;
} else if payload_obj.contains_key("model_mesh") {
handle_model_mesh_payload(payload_obj, &job, &server_state).await?;
}
}
@@ -0,0 +1,102 @@
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
use crate::state::server_state::ServerState;
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
use crate::util::http_download_url_to_tempfile::http_download_url_to_tempfile;
use actix_web::web;
use anyhow::anyhow;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use errors::AnyhowResult;
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
use images::image_info::image_info::ImageInfo;
use log::{info, warn};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use mysql_queries::queries::generic_inference::web::get_inference_job_by_fal_id::FalJobDetails;
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
use serde_json::{Map, Value};
use std::sync::Arc;
const PREFIX : Option<&str> = Some("artcraft_");
#[derive(Deserialize, Debug)]
pub struct FalWebhookModelMesh {
pub content_type: Option<String>,
pub file_name: Option<String>,
pub file_size: Option<usize>,
pub url: Option<String>,
}
pub async fn handle_model_mesh_payload(
payload: &Map<String, Value>,
job: &FalJobDetails,
server_state: &ServerState,
) -> AnyhowResult<()> {
let model_mesh_value = payload.get("model_mesh")
.ok_or_else(|| anyhow!("no `model_mesh` key in payload"))?;
info!("Fal Model Mesh Payload: {:?}", model_mesh_value);
let mesh: FalWebhookModelMesh = serde_json::from_value(model_mesh_value.clone())?;
let mesh_url = mesh.url
.as_deref()
.ok_or_else(|| anyhow!("no `url` in image payload"))?;
//let mime_type = mesh.content_type
// .as_deref()
// .ok_or_else(|| anyhow!("no `content_type` in mesh payload"))?;
let file_bytes = http_download_url_to_bytes(mesh_url)
.await
.map_err(|e| anyhow!("Failed to download mesh: {:?}", e))?;
let mimetype_info = MimetypeInfo::get_for_bytes(&file_bytes)
.ok_or_else(|| anyhow!("Failed to get mimetype info"))?;
let mime_type = mimetype_info.mime_type();
info!("Mime type of mesh: {}", mime_type);
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
.ok_or_else(|| anyhow!("Unsupported media file type: {}", mime_type))?;
let extension_with_period = mimetype_info.file_extension()
.map(|ext| ext.extension_with_period())
.ok_or_else(|| anyhow!("Failed to get file extension from mimetype info"))?;
let file_size_bytes = file_bytes.len();
let file_hash = sha256_hash_bytes(&file_bytes)?;
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
info!("Uploading media to bucket path: {}", public_upload_path.get_full_object_path_str());
server_state.public_bucket_client.upload_file_with_content_type_process(
public_upload_path.get_full_object_path_str(),
file_bytes.as_ref(),
&mime_type)
.await?;
let media_token = MediaFileInsertBuilder::new()
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
.creator_ip_address(&job.creator_ip_address)
.public_bucket_directory_hash(&public_upload_path)
.media_file_class(MediaFileClass::Dimensional)
.media_file_type(media_file_type)
.media_file_origin_category(MediaFileOriginCategory::Inference)
//.media_file_origin_product_category(MediaFileOriginProductCategory::Unknown)
.mime_type(mime_type)
.file_size_bytes(file_size_bytes as u64)
.checksum_sha2(&file_hash)
.insert_pool(&server_state.mysql_pool)
.await?;
info!("Mesh media file uploaded with token: {}", media_token);
Ok(())
}
@@ -1,3 +1,4 @@
pub mod fal_webhook_handler;
pub mod handle_image_payload;
pub mod handle_model_mesh_payload;
pub mod handle_video_payload;
@@ -1,9 +1,10 @@
use crate::http_server::endpoints::generate::image::remove_image_background_handler::remove_image_background_handler;
use crate::http_server::endpoints::generate::object::generate_hunyuan2_image_to_3d_handler::generate_hunyuan_2_image_to_3d_handler;
use crate::http_server::endpoints::generate::video::generate_kling_1_6_video_handler::generate_kling_1_6_video_handler;
use actix_http::body::MessageBody;
use actix_service::ServiceFactory;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::{web, App, Error, HttpResponse};
use crate::http_server::endpoints::generate::video::generate_kling_1_6_video_handler::generate_kling_1_6_video_handler;
pub fn add_generate_routes<T, B> (app: App<T>) -> App<T>
where
@@ -29,5 +30,11 @@ where
.route(web::head().to(|| HttpResponse::Ok()))
)
)
.service(web::scope("/object")
.service(web::resource("/hunyuan_2_image_to_3d")
.route(web::post().to(generate_hunyuan_2_image_to_3d_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
)
}