diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/enqueue_fbx_to_gltf_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/enqueue_fbx_to_gltf_handler.rs new file mode 100644 index 0000000000..1225df1b24 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/enqueue_fbx_to_gltf_handler.rs @@ -0,0 +1,216 @@ +#![forbid(unused_mut)] + +use std::fmt::Debug; +use std::sync::Arc; + +use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use log::warn; +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory; +use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType; +use enums::common::visibility::Visibility; +use http_server_common::request::get_request_header_optional::get_request_header_optional; +use http_server_common::request::get_request_ip::get_request_ip; +use mysql_queries::payloads::generic_inference_args::generic_inference_args::{ + GenericInferenceArgs, + InferenceCategoryAbbreviated, +}; +use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{ + insert_generic_inference_job, + InsertGenericInferenceArgs, +}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use tokens::tokens::users::UserToken; + +use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session; +use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; +use crate::server_state::ServerState; + +/// Debug requests can get routed to special "debug-only" workers, which can +/// be used to trial new code, run debugging, etc. +const DEBUG_HEADER_NAME: &str = "enable-debug-mode"; + +/// The routing tag header can send workloads to particular k8s hosts. +/// This is useful for catching the live logs or intercepting the job. +const ROUTING_TAG_HEADER_NAME: &str = "routing-tag"; + +#[derive(Deserialize, ToSchema)] +pub struct EnqueueFbxToGltfRequest { + // Entropy for idempotency + uuid_idempotency_token: String, + // The existing FBX media file token + media_file_token: MediaFileToken, +} + +#[derive(Serialize, ToSchema)] +pub struct EnqueueFbxToGltfRequestSuccessResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} + +#[derive(Debug, ToSchema)] +pub enum EnqueueFbxToGltfRequestError { + BadInput(String), + NotAuthorized, + ServerError, + RateLimited, +} + +impl ResponseError for EnqueueFbxToGltfRequestError { + fn status_code(&self) -> StatusCode { + match *self { + EnqueueFbxToGltfRequestError::BadInput(_) => StatusCode::BAD_REQUEST, + EnqueueFbxToGltfRequestError::NotAuthorized => StatusCode::UNAUTHORIZED, + EnqueueFbxToGltfRequestError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + EnqueueFbxToGltfRequestError::RateLimited => StatusCode::TOO_MANY_REQUESTS, + } + } + + fn error_response(&self) -> HttpResponse { + let error_reason = match self { + EnqueueFbxToGltfRequestError::BadInput(reason) => reason.to_string(), + EnqueueFbxToGltfRequestError::NotAuthorized => "unauthorized".to_string(), + EnqueueFbxToGltfRequestError::ServerError => "server error".to_string(), + EnqueueFbxToGltfRequestError::RateLimited => "rate limited".to_string(), + }; + + to_simple_json_error(&error_reason, self.status_code()) + } +} + +impl std::fmt::Display for EnqueueFbxToGltfRequestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[utoipa::path( + post, + path = "/v1/conversion/enqueue_fbx_to_gltf", + responses( + ( + status = 200, + description = "Enqueue FBX to GLTF", + body = EnqueueFbxToGltfRequestSuccessResponse, + ), + (status = 400, description = "Bad input", body = EnqueueFbxToGltfRequestError), + (status = 401, description = "Not authorized", body = EnqueueFbxToGltfRequestError), + (status = 429, description = "Rate limited", body = EnqueueFbxToGltfRequestError), + (status = 500, description = "Server error", body = EnqueueFbxToGltfRequestError) + ), + params(("request" = EnqueueFbxToGltfRequest, description = "Payload for FBX to GLTF Request")) +)] +pub async fn enqueue_fbx_to_gltf_handler( + http_request: HttpRequest, + request: web::Json, + server_state: web::Data> +) -> Result { + let mut maybe_user_token: Option = None; + + let mut mysql_connection = server_state.mysql_pool.acquire().await.map_err(|err| { + warn!("MySql pool error: {:?}", err); + EnqueueFbxToGltfRequestError::ServerError + })?; + + // ==================== USER SESSION ==================== // + + let maybe_user_session = server_state.session_checker + .maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection).await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + EnqueueFbxToGltfRequestError::ServerError + })?; + + if let Some(user_session) = maybe_user_session.as_ref() { + maybe_user_token = Some(UserToken::new_from_str(&user_session.user_token)); + } + + // Plan should handle "first anonymous use" and "investor" cases. + let plan = get_correct_plan_for_session( + server_state.server_environment, + maybe_user_session.as_ref() + ); + + // Separate priority for animation. + let priority_level = plan.web_vc_base_priority_level(); + + // ==================== DEBUG MODE + ROUTING TAG ==================== // + + let is_debug_request = get_request_header_optional(&http_request, DEBUG_HEADER_NAME).is_some(); + + let maybe_routing_tag = get_request_header_optional(&http_request, ROUTING_TAG_HEADER_NAME).map( + |routing_tag| routing_tag.trim().to_string() + ); + + // ==================== BANNED USERS ==================== // + + if let Some(ref user) = maybe_user_session { + if user.role.is_banned { + return Err(EnqueueFbxToGltfRequestError::NotAuthorized); + } + } + + // ==================== RATE LIMIT ==================== // + + let rate_limiter = match maybe_user_session { + None => &server_state.redis_rate_limiters.logged_out, + Some(ref _user) => &server_state.redis_rate_limiters.logged_in, + }; + + if let Err(_err) = rate_limiter.rate_limit_request(&http_request) { + return Err(EnqueueFbxToGltfRequestError::RateLimited); + } + + let ip_address = get_request_ip(&http_request); + + let maybe_avt_token = server_state.avt_cookie_manager + .get_avt_token_from_request(&http_request); + + let query_result = insert_generic_inference_job(InsertGenericInferenceArgs { + uuid_idempotency_token: &request.uuid_idempotency_token, + inference_category: InferenceCategory::ImageGeneration, + maybe_model_type: Some(InferenceModelType::ConvertFbxToGltf), + maybe_model_token: None, + maybe_input_source_token: Some(&request.media_file_token.as_str()), + maybe_input_source_token_type: None, + maybe_raw_inference_text: None, + maybe_max_duration_seconds: None, + maybe_inference_args: Some(GenericInferenceArgs { + inference_category: Some(InferenceCategoryAbbreviated::FormatConversion), + args: None, // NB: no format conversion args for now + }), + maybe_creator_user_token: maybe_user_token.as_ref(), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + priority_level, + requires_keepalive: true, + is_debug_request, + maybe_routing_tag: maybe_routing_tag.as_deref(), + mysql_pool: &server_state.mysql_pool, + }).await; + + let job_token = match query_result { + Ok((job_token, _id)) => job_token, + Err(err) => { + warn!("New generic inference job creation DB error: {:?}", err); + return Err(EnqueueFbxToGltfRequestError::ServerError); + } + }; + + let response: EnqueueFbxToGltfRequestSuccessResponse = EnqueueFbxToGltfRequestSuccessResponse { + success: true, + inference_job_token: job_token, + }; + + let body = serde_json::to_string(&response) + .map_err(|_e| EnqueueFbxToGltfRequestError::ServerError)?; + + Ok(HttpResponse::Ok().content_type("application/json").body(body)) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/mod.rs new file mode 100644 index 0000000000..9340f80623 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/conversion/mod.rs @@ -0,0 +1 @@ +pub mod enqueue_fbx_to_gltf_handler; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs index c9b64c5dac..8e1e55a6e2 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs @@ -2,9 +2,11 @@ pub mod animation; pub mod api_tokens; pub mod categories; pub mod comments; +pub mod conversion; pub mod download_job; pub mod events; pub mod flags; +pub mod image_gen; pub mod inference_job; pub mod investor_demo; pub mod leaderboard; @@ -19,14 +21,13 @@ pub mod subscriptions; pub mod trending; pub mod tts; pub mod twitch; +pub mod user_bookmarks; pub mod user_ratings; pub mod vocoders; pub mod voice_clone_requests; pub mod voice_conversion; pub mod voice_designer; pub mod w2l; -pub mod user_bookmarks; pub mod weights; -pub mod image_gen; pub(crate) mod mocap; pub(crate) mod workflows; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/inference/enqueue_tts_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/inference/enqueue_tts_request.rs index 5a8e7bb3f0..095b83ce1e 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/inference/enqueue_tts_request.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/inference/enqueue_tts_request.rs @@ -15,6 +15,7 @@ use utoipa::ToSchema; use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory; use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType; +use enums::common::visibility::Visibility; use http_server_common::request::get_request_header_optional::get_request_header_optional; use http_server_common::request::get_request_ip::get_request_ip; use mysql_queries::payloads::generic_inference_args::generic_inference_args::{ @@ -202,7 +203,7 @@ pub async fn enqueue_tts_request( maybe_creator_user_token: maybe_user_token.as_ref(), maybe_avt_token: maybe_avt_token.as_ref(), creator_ip_address: &ip_address, - creator_set_visibility: enums::common::visibility::Visibility::Public, + creator_set_visibility: Visibility::Public, priority_level, requires_keepalive: true, // do we need this? I think so is_debug_request, diff --git a/crates/service/web/storyteller_web/src/routes.rs b/crates/service/web/storyteller_web/src/routes.rs index 862cc8ea07..81f4292803 100644 --- a/crates/service/web/storyteller_web/src/routes.rs +++ b/crates/service/web/storyteller_web/src/routes.rs @@ -26,11 +26,13 @@ use crate::http_server::endpoints::categories::tts::list_tts_model_assigned_cate use crate::http_server::endpoints::comments::create_comment_handler::create_comment_handler; use crate::http_server::endpoints::comments::delete_comment_handler::delete_comment_handler; use crate::http_server::endpoints::comments::list_comments_handler::list_comments_handler; +use crate::http_server::endpoints::conversion::enqueue_fbx_to_gltf_handler::enqueue_fbx_to_gltf_handler; use crate::http_server::endpoints::download_job::enqueue_generic_download::enqueue_generic_download_handler; use crate::http_server::endpoints::download_job::get_generic_upload_job_status::get_generic_download_job_status_handler; use crate::http_server::endpoints::events::list_events::list_events_handler; use crate::http_server::endpoints::flags::design_refresh_flag::disable_design_refresh_flag_handler::disable_design_refresh_flag_handler; use crate::http_server::endpoints::flags::design_refresh_flag::enable_design_refresh_flag_handler::enable_design_refresh_flag_handler; +use crate::http_server::endpoints::image_gen::enqueue_image_generation::enqueue_image_generation_request; use crate::http_server::endpoints::inference_job::get_inference_job_status::get_inference_job_status_handler; use crate::http_server::endpoints::inference_job::get_pending_inference_job_count::get_pending_inference_job_count_handler; use crate::http_server::endpoints::inference_job::kill_inference_jobs::kill_generic_inference_jobs_handler; @@ -55,7 +57,6 @@ use crate::http_server::endpoints::misc::enable_alpha_easy_handler::enable_alpha use crate::http_server::endpoints::misc::enable_alpha_handler::enable_alpha_handler; use crate::http_server::endpoints::misc::root_index::get_root_index; use crate::http_server::endpoints::mocap::enqueue_mocapnet::enqueue_mocapnet_handler; -use crate::http_server::endpoints::workflows::enqueue_comfy_ui::enqueue_comfy_ui_handler; use crate::http_server::endpoints::moderation::approval::pending_w2l_templates::get_pending_w2l_templates_handler; use crate::http_server::endpoints::moderation::categories::delete_category::delete_category_handler; use crate::http_server::endpoints::moderation::categories::edit_category::edit_category_handler; @@ -167,7 +168,7 @@ use crate::http_server::endpoints::weights::list_weights_by_user::list_weights_b use crate::http_server::endpoints::weights::search_model_weights_handler::search_model_weights_handler; use crate::http_server::endpoints::weights::set_model_weight_cover_image::set_model_weight_cover_image_handler; use crate::http_server::endpoints::weights::update_weight::update_weight_handler; -use crate::http_server::endpoints::image_gen::enqueue_image_generation::enqueue_image_generation_request; +use crate::http_server::endpoints::workflows::enqueue_comfy_ui::enqueue_comfy_ui_handler; pub fn add_routes (app: App, server_environment: ServerEnvironment) -> App where @@ -250,6 +251,12 @@ pub fn add_routes (app: App, server_environment: ServerEnvironment) -> .add_get("/v1/model_inference/queue_length", get_pending_inference_job_count_handler) .into_app(); + // ==================== Format Conversion ==================== + + let mut app = RouteBuilder::from_app(app) + .add_post("/v1/conversion/enqueue_fbx_to_gltf", enqueue_fbx_to_gltf_handler) + .into_app(); + // ==================== Stats ==================== let mut app = RouteBuilder::from_app(app)