artcraft_router handler impl, wip

This commit is contained in:
Brandon Thomas committed 2026-04-05 23:32:32 -04:00
1 parent d6d3d86c76
commit d2e545fa46
8 files changed
+520 -24

No files matched your search

@@ -1,2 +1,4 @@
pub mod omni_gen_image_generate_handler;
mod request_to_costs;
mod request_to_execution_plan;
mod transform_request;
@@ -2,14 +2,36 @@ use std::sync::Arc;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use log::warn;
use log::{error, info, warn};
use sqlx::Acquire;
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
use artcraft_api_defs::omni_gen::generate_response::omni_gen_image_generate_response::OmniGenImageGenerateResponse;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation::common_generation_mode::CommonGenerationMode;
use enums::common::generation_provider::GenerationProvider;
use enums::common::visibility::Visibility;
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_with_apriori_job_token::{
insert_generic_inference_job_for_fal_queue_with_apriori_job_token,
InsertGenericInferenceForFalWithAprioriJobTokenArgs,
};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
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 tokens::tokens::generic_inference_jobs::InferenceJobToken;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::endpoints::omni_gen::generate::image::transform_request::transform_request;
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 super::request_to_costs::estimate_costs;
/// Generate an image using the omni-gen unified endpoint.
#[utoipa::path(
@@ -26,17 +48,187 @@ use crate::state::server_state::ServerState;
),
)]
pub async fn omni_gen_image_generate_handler(
_http_request: HttpRequest,
http_request: HttpRequest,
request: Json<OmniGenImageCostAndGenerateRequest>,
_server_state: web::Data<Arc<ServerState>>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<OmniGenImageGenerateResponse>, AdvancedCommonWebError> {
let generate_request = transform_request(&request)?;
let plan = generate_request.build()
.map_err(|err| {
warn!("Invalid image generation request. Error: {:?}, Request: {:?}", err, generate_request);
AdvancedCommonWebError::from_error(err)
})?;
todo!()
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
// ==================== SESSION ==================== //
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
AdvancedCommonWebError::from_anyhow_error(e)
})?;
let user_token = match maybe_user_session.as_ref() {
Some(session) => &session.user_token,
None => return Err(AdvancedCommonWebError::NotAuthorized),
};
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// ==================== IDEMPOTENCY ==================== //
let idempotency_token = request.idempotency_token.as_deref()
.unwrap_or("")
.to_string();
if let Err(reason) = validate_idempotency_token_format(&idempotency_token) {
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
AdvancedCommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
})?;
// ==================== COST + BILLING ==================== //
let cost_estimate = estimate_costs(&request)?;
let cost = cost_estimate.cost_in_credits.unwrap_or(0);
info!("Charging wallet: {} credits", cost);
let apriori_job_token = InferenceJobToken::generate();
if cost > 0 {
attempt_wallet_deduction_else_common_web_error(
user_token,
Some(apriori_job_token.as_str()),
cost,
&mut mysql_connection,
).await?;
}
// ==================== EXECUTE GENERATION ==================== //
// TODO: Execute the generation plan via the router client.
// For now, we record the job as pending without dispatching externally.
let external_job_id = "";
// ==================== DB TRANSACTION ==================== //
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
AdvancedCommonWebError::from_error(err)
})?;
// -- Prompt --
let generation_mode = if request.image_media_tokens.is_some() {
CommonGenerationMode::Edit
} else {
CommonGenerationMode::Text
};
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: Some(user_token),
maybe_model_type: None,
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: None,
maybe_other_args: None,
maybe_generation_mode: Some(generation_mode),
maybe_aspect_ratio: None,
maybe_resolution: None,
maybe_batch_count: request.image_batch_count.map(|c| c as u8),
maybe_generate_audio: None,
maybe_duration_seconds: 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
}
};
// -- Prompt context items --
if let Some(token) = prompt_token.as_ref() {
let mut context_items = Vec::new();
if let Some(ref_tokens) = &request.image_media_tokens {
for media_token in ref_tokens {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::Imgsrc,
});
}
}
if !context_items.is_empty() {
if let Err(err) = insert_batch_prompt_context_items(InsertBatchArgs {
prompt_token: token.clone(),
items: context_items,
transaction: &mut transaction,
}).await {
warn!("Error inserting batch prompt context items: {:?}", err);
}
}
}
// -- Inference job --
let db_result = insert_generic_inference_job_for_fal_queue_with_apriori_job_token(
InsertGenericInferenceForFalWithAprioriJobTokenArgs {
apriori_job_token: &apriori_job_token,
uuid_idempotency_token: &idempotency_token,
maybe_external_third_party_id: external_job_id,
fal_category: FalCategory::ImageGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: Some(user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}
).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting inference job: {:?}", err);
return Err(AdvancedCommonWebError::from_error(err));
}
};
transaction.commit().await.map_err(|err| {
error!("Error committing transaction: {:?}", err);
AdvancedCommonWebError::from_error(err)
})?;
Ok(Json(OmniGenImageGenerateResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -0,0 +1,24 @@
use artcraft_router::api::provider::Provider;
use artcraft_router::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use log::warn;
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::endpoints::omni_gen::generate::image::transform_request::transform_request;
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
/// Build the Artcraft-provider cost plan for this request.
pub fn estimate_costs(
request: &OmniGenImageCostAndGenerateRequest,
) -> Result<ImageGenerationCostEstimate, AdvancedCommonWebError> {
let mut generate_request = transform_request(request)?;
generate_request.provider = Provider::Artcraft;
let plan = generate_request.build()
.map_err(|e| {
warn!("Failed to build cost plan: {}", e);
AdvancedCommonWebError::from_error(e)
})?;
Ok(plan.estimate_costs())
}
@@ -0,0 +1,18 @@
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::endpoints::omni_gen::generate::image::transform_request::transform_request;
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
/// Validate that an execution plan can be built for this request.
/// Returns an error if the model/provider combination is unsupported.
pub fn validate_execution_plan(
request: &OmniGenImageCostAndGenerateRequest,
) -> Result<(), AdvancedCommonWebError> {
let generate_request = transform_request(request)?;
generate_request.build()
.map(|_| ())
.map_err(|e| AdvancedCommonWebError::BadInputWithSimpleMessage(
format!("Failed to build execution plan: {}", e),
))
}
@@ -1,2 +1,4 @@
pub mod omni_gen_video_generate_handler;
mod request_to_costs;
mod request_to_execution_plan;
mod transform_request;
@@ -2,14 +2,37 @@ use std::sync::Arc;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use log::warn;
use log::{error, info, warn};
use sqlx::Acquire;
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
use artcraft_api_defs::omni_gen::generate_response::omni_gen_video_generate_response::OmniGenVideoGenerateResponse;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation::common_generation_mode::CommonGenerationMode;
use enums::common::generation_provider::GenerationProvider;
use enums::common::visibility::Visibility;
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_with_apriori_job_token::{
insert_generic_inference_job_for_fal_queue_with_apriori_job_token,
InsertGenericInferenceForFalWithAprioriJobTokenArgs,
};
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
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 tokens::tokens::generic_inference_jobs::InferenceJobToken;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::endpoints::omni_gen::generate::video::transform_request::transform_request;
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 super::request_to_costs::estimate_costs;
/// Generate a video using the omni-gen unified endpoint.
#[utoipa::path(
post,
@@ -25,17 +48,210 @@ use crate::state::server_state::ServerState;
),
)]
pub async fn omni_gen_video_generate_handler(
_http_request: HttpRequest,
http_request: HttpRequest,
request: Json<OmniGenVideoCostAndGenerateRequest>,
_server_state: web::Data<Arc<ServerState>>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<OmniGenVideoGenerateResponse>, AdvancedCommonWebError> {
let generate_request = transform_request(&request)?;
let plan = generate_request.build()
.map_err(|err| {
warn!("Invalid video generation request. Error: {:?}, Request: {:?}", err, generate_request);
AdvancedCommonWebError::from_error(err)
})?;
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
todo!()
// ==================== SESSION ==================== //
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
AdvancedCommonWebError::from_anyhow_error(e)
})?;
let user_token = match maybe_user_session.as_ref() {
Some(session) => &session.user_token,
None => return Err(AdvancedCommonWebError::NotAuthorized),
};
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// ==================== IDEMPOTENCY ==================== //
let idempotency_token = request.idempotency_token.as_deref()
.unwrap_or("")
.to_string();
if let Err(reason) = validate_idempotency_token_format(&idempotency_token) {
return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(reason));
}
insert_idempotency_token(&idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
AdvancedCommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
})?;
// ==================== COST + BILLING ==================== //
let cost_estimate = estimate_costs(&request)?;
let cost = cost_estimate.cost_in_credits.unwrap_or(0);
info!("Charging wallet: {} credits", cost);
let apriori_job_token = InferenceJobToken::generate();
if cost > 0 {
attempt_wallet_deduction_else_common_web_error(
user_token,
Some(apriori_job_token.as_str()),
cost,
&mut mysql_connection,
).await?;
}
// ==================== EXECUTE GENERATION ==================== //
// TODO: Execute the generation plan via the router client.
// For now, we record the job as pending without dispatching externally.
let external_job_id = "";
// ==================== DB TRANSACTION ==================== //
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
AdvancedCommonWebError::from_error(err)
})?;
// -- Prompt --
let generation_mode = if request.start_frame_image_media_token.is_some() {
CommonGenerationMode::Keyframe
} else {
CommonGenerationMode::Text
};
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: Some(user_token),
maybe_model_type: None,
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: request.negative_prompt.as_deref(),
maybe_other_args: None,
maybe_generation_mode: Some(generation_mode),
maybe_aspect_ratio: None,
maybe_resolution: None,
maybe_batch_count: request.video_batch_count.map(|c| c as u8),
maybe_generate_audio: request.generate_audio,
maybe_duration_seconds: request.duration_seconds.map(|d| d as u32),
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
}
};
// -- Prompt context items --
if let Some(token) = prompt_token.as_ref() {
let mut context_items = Vec::new();
if let Some(media_token) = &request.start_frame_image_media_token {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidStartFrame,
});
}
if let Some(media_token) = &request.end_frame_image_media_token {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidEndFrame,
});
}
if let Some(ref_tokens) = &request.reference_image_media_tokens {
for media_token in ref_tokens {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::Imgref,
});
}
}
if let Some(ref_tokens) = &request.reference_video_media_tokens {
for media_token in ref_tokens {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidRef,
});
}
}
if !context_items.is_empty() {
if let Err(err) = insert_batch_prompt_context_items(InsertBatchArgs {
prompt_token: token.clone(),
items: context_items,
transaction: &mut transaction,
}).await {
warn!("Error inserting batch prompt context items: {:?}", err);
}
}
}
// -- Inference job --
let db_result = insert_generic_inference_job_for_fal_queue_with_apriori_job_token(
InsertGenericInferenceForFalWithAprioriJobTokenArgs {
apriori_job_token: &apriori_job_token,
uuid_idempotency_token: &idempotency_token,
maybe_external_third_party_id: external_job_id,
fal_category: FalCategory::VideoGeneration,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: Some(user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
phantom: Default::default(),
}
).await;
let job_token = match db_result {
Ok(token) => token,
Err(err) => {
warn!("Error inserting inference job: {:?}", err);
return Err(AdvancedCommonWebError::from_error(err));
}
};
transaction.commit().await.map_err(|err| {
error!("Error committing transaction: {:?}", err);
AdvancedCommonWebError::from_error(err)
})?;
Ok(Json(OmniGenVideoGenerateResponse {
success: true,
inference_job_token: job_token,
}))
}
@@ -0,0 +1,24 @@
use artcraft_router::api::provider::Provider;
use artcraft_router::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use log::warn;
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::endpoints::omni_gen::generate::video::transform_request::transform_request;
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
/// Build the Artcraft-provider cost plan for this request.
pub fn estimate_costs(
request: &OmniGenVideoCostAndGenerateRequest,
) -> Result<VideoGenerationCostEstimate, AdvancedCommonWebError> {
let mut generate_request = transform_request(request)?;
generate_request.provider = Provider::Artcraft;
let plan = generate_request.build()
.map_err(|e| {
warn!("Failed to build cost plan: {}", e);
AdvancedCommonWebError::from_error(e)
})?;
Ok(plan.estimate_costs())
}
@@ -0,0 +1,18 @@
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
use crate::http_server::endpoints::omni_gen::generate::video::transform_request::transform_request;
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
/// Validate that an execution plan can be built for this request.
/// Returns an error if the model/provider combination is unsupported.
pub fn validate_execution_plan(
request: &OmniGenVideoCostAndGenerateRequest,
) -> Result<(), AdvancedCommonWebError> {
let generate_request = transform_request(request)?;
generate_request.build()
.map(|_| ())
.map_err(|e| AdvancedCommonWebError::BadInputWithSimpleMessage(
format!("Failed to build execution plan: {}", e),
))
}