Compare commits

...
Author SHA1 Message Date
Brandon Thomas da15c4b2d9 log missing tokens 2025-12-05 21:08:39 -05:00
Brandon Thomas cbabbfcfa8 route + client 2025-12-05 19:23:13 -05:00
Brandon Thomas eea7c2dd2e nano banana endpoint (almost done) 2025-12-05 19:19:10 -05:00
11 changed files with 495 additions and 2 deletions
@@ -5,4 +5,5 @@ pub mod generate_flux_pro_11_text_to_image;
pub mod generate_flux_pro_11_ultra_text_to_image;
pub mod generate_gpt_image_1_text_to_image;
pub mod inpaint;
pub mod multi_function;
pub mod remove_image_background;
@@ -0,0 +1 @@
pub mod nano_banana_pro_multi_function_image_gen;
@@ -0,0 +1,71 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const NANO_BANANA_PRO_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/nano_banana_pro";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct NanoBananaProMultiFunctionImageGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Image media tokens to include in the editing context.
/// If present, we're doing image editing (image-to-image / image-editing)
/// If absent, we're doing image generation (text-to-image)
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<NanoBananaProMultiFunctionImageGenNumImages>,
/// Resolution of the image to generate. Default is OneK (1K).
pub resolution: Option<NanoBananaProMultiFunctionImageGenImageResolution>,
/// Aspect ratio of the images to generate. Default is "1:1"
pub aspect_ratio: Option<NanoBananaProMultiFunctionImageGenAspectRatio>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum NanoBananaProMultiFunctionImageGenNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum NanoBananaProMultiFunctionImageGenImageResolution {
OneK,
TwoK,
FourK,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum NanoBananaProMultiFunctionImageGenAspectRatio {
// Square
OneByOne, // Default
// Wide
FiveByFour,
FourByThree,
ThreeByTwo,
SixteenByNine,
TwentyOneByNine,
// Tall
FourByFive,
ThreeByFour,
TwoByThree,
NineBySixteen, // NB: No NineByTwentyOne ?
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct NanoBananaProMultiFunctionImageGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -5,4 +5,5 @@ pub mod generate_flux_pro_11_text_to_image;
pub mod generate_flux_pro_11_ultra_text_to_image;
pub mod generate_gpt_image_1_text_to_image;
pub mod inpaint;
pub mod multi_function;
pub mod remove_image_background;
@@ -0,0 +1 @@
pub mod nano_banana_pro_multi_function_image_gen_image;
@@ -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::image::multi_function::nano_banana_pro_multi_function_image_gen::{NanoBananaProMultiFunctionImageGenRequest, NanoBananaProMultiFunctionImageGenResponse, NANO_BANANA_PRO_MULTI_FUNCTION_IMAGE_GEN_PATH};
pub async fn nano_banana_pro_multi_function_image_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: NanoBananaProMultiFunctionImageGenRequest,
) -> Result<NanoBananaProMultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
NANO_BANANA_PRO_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -38,8 +38,13 @@ pub enum ModelType {
SeedEdit3,
#[serde(rename = "qwen")]
Qwen,
/// Gemini 2.5 Flash, AKA "Nano Banana"
#[serde(rename = "gemini_25_flash")]
Gemini25Flash,
#[serde(rename = "nano_banana")]
NanoBanana,
#[serde(rename = "nano_banana_pro")]
NanoBananaPro,
/// Midjourney without distinguishing a model type or version
#[serde(rename = "midjourney")]
@@ -117,6 +122,8 @@ impl ModelType {
Self::SeedEdit3 => "seededit_3",
Self::Qwen => "qwen",
Self::Gemini25Flash => "gemini_25_flash",
Self::NanoBanana => "nano_banana",
Self::NanoBananaPro => "nano_banana_pro",
Self::Midjourney => "midjourney",
Self::MidjourneyV6 => "midjourney_v6",
Self::MidjourneyV6p1 => "midjourney_v6p1",
@@ -160,6 +167,8 @@ impl ModelType {
"seededit_3" => Ok(Self::SeedEdit3),
"qwen" => Ok(Self::Qwen),
"gemini_25_flash" => Ok(Self::Gemini25Flash),
"nano_banana" => Ok(Self::NanoBanana),
"nano_banana_pro" => Ok(Self::NanoBananaPro),
"midjourney" => Ok(Self::Midjourney),
"midjourney_v6" => Ok(Self::MidjourneyV6),
"midjourney_v6p1" => Ok(Self::MidjourneyV6p1),
@@ -207,6 +216,8 @@ impl ModelType {
Self::SeedEdit3,
Self::Qwen,
Self::Gemini25Flash,
Self::NanoBanana,
Self::NanoBananaPro,
Self::Midjourney,
Self::MidjourneyV6,
Self::MidjourneyV6p1,
@@ -259,6 +270,8 @@ mod tests {
assert_serialization(ModelType::SeedEdit3, "seededit_3");
assert_serialization(ModelType::Qwen, "qwen");
assert_serialization(ModelType::Gemini25Flash, "gemini_25_flash");
assert_serialization(ModelType::NanoBanana, "nano_banana");
assert_serialization(ModelType::NanoBananaPro, "nano_banana_pro");
assert_serialization(ModelType::Midjourney, "midjourney");
assert_serialization(ModelType::MidjourneyV6, "midjourney_v6");
assert_serialization(ModelType::MidjourneyV6p1, "midjourney_v6p1");
@@ -299,6 +312,8 @@ mod tests {
assert_eq!(ModelType::SeedEdit3.to_str(), "seededit_3");
assert_eq!(ModelType::Qwen.to_str(), "qwen");
assert_eq!(ModelType::Gemini25Flash.to_str(), "gemini_25_flash");
assert_eq!(ModelType::NanoBanana.to_str(), "nano_banana");
assert_eq!(ModelType::NanoBananaPro.to_str(), "nano_banana_pro");
assert_eq!(ModelType::Midjourney.to_str(), "midjourney");
assert_eq!(ModelType::MidjourneyV6.to_str(), "midjourney_v6");
assert_eq!(ModelType::MidjourneyV6p1.to_str(), "midjourney_v6p1");
@@ -341,6 +356,8 @@ mod tests {
assert_eq!(ModelType::from_str("seededit_3").unwrap(), ModelType::SeedEdit3);
assert_eq!(ModelType::from_str("qwen").unwrap(), ModelType::Qwen);
assert_eq!(ModelType::from_str("gemini_25_flash").unwrap(), ModelType::Gemini25Flash);
assert_eq!(ModelType::from_str("nano_banana").unwrap(), ModelType::NanoBanana);
assert_eq!(ModelType::from_str("nano_banana_pro").unwrap(), ModelType::NanoBananaPro);
assert_eq!(ModelType::from_str("midjourney").unwrap(), ModelType::Midjourney);
assert_eq!(ModelType::from_str("midjourney_v6").unwrap(), ModelType::MidjourneyV6);
assert_eq!(ModelType::from_str("midjourney_v6p1").unwrap(), ModelType::MidjourneyV6p1);
@@ -368,7 +385,7 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = ModelType::all_variants();
assert_eq!(variants.len(), 33);
assert_eq!(variants.len(), 35);
// Image models
assert_eq!(variants.pop_first(), Some(ModelType::Flux1Dev));
assert_eq!(variants.pop_first(), Some(ModelType::Flux1Schnell));
@@ -383,6 +400,8 @@ mod tests {
assert_eq!(variants.pop_first(), Some(ModelType::SeedEdit3));
assert_eq!(variants.pop_first(), Some(ModelType::Qwen));
assert_eq!(variants.pop_first(), Some(ModelType::Gemini25Flash));
assert_eq!(variants.pop_first(), Some(ModelType::NanoBanana));
assert_eq!(variants.pop_first(), Some(ModelType::NanoBananaPro));
assert_eq!(variants.pop_first(), Some(ModelType::Midjourney));
assert_eq!(variants.pop_first(), Some(ModelType::MidjourneyV6));
assert_eq!(variants.pop_first(), Some(ModelType::MidjourneyV6p1));
@@ -5,4 +5,5 @@ pub mod generate_flux_pro_11_text_to_image_handler;
pub mod generate_flux_pro_11_ultra_text_to_image_handler;
pub mod generate_gpt_image_1_text_to_image_handler;
pub mod inpaint;
pub mod remove_image_background_handler;
pub mod remove_image_background_handler;
pub mod multi_function;
@@ -0,0 +1 @@
pub mod nano_banana_pro_multi_function_image_gen_handler;
@@ -0,0 +1,372 @@
use std::collections::HashSet;
use std::iter::FromIterator;
use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{NanoBananaProMultiFunctionImageGenAspectRatio, NanoBananaProMultiFunctionImageGenImageResolution, NanoBananaProMultiFunctionImageGenNumImages, NanoBananaProMultiFunctionImageGenRequest, NanoBananaProMultiFunctionImageGenResponse};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
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::creds::open_ai_api_key::OpenAiApiKey;
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::{enqueue_nano_banana_pro_image_edit_webhook, EnqueueNanoBananaProEditImageArgs, EnqueueNanoBananaProEditImageAspectRatio, EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageResolution};
use fal_client::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::{enqueue_nano_banana_pro_text_to_image_webhook, EnqueueNanoBananaProTextToImageArgs, EnqueueNanoBananaProTextToImageAspectRatio, EnqueueNanoBananaProTextToImageNumImages, EnqueueNanoBananaProTextToImageResolution};
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;
use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory;
use mysql_queries::queries::generic_inference::fal::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::batch_get_media_files_by_tokens::{batch_get_media_files_by_tokens, batch_get_media_files_by_tokens_with_connection};
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 server_environment::ServerEnvironment;
use sqlx::pool::PoolConnection;
use sqlx::{Acquire, MySql};
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
/// Nano Banana Pro Multi-Function (generate + edit)
#[utoipa::path(
post,
tag = "Generate Images (Multi-Function)",
path = "/v1/generate/image/multi_function/nano_banana_pro",
responses(
(status = 200, description = "Success", body = NanoBananaProMultiFunctionImageGenResponse),
),
params(
("request" = NanoBananaProMultiFunctionImageGenRequest, description = "Payload for Request"),
)
)]
pub async fn nano_banana_pro_multi_function_image_gen_handler(
http_request: HttpRequest,
request: Json<NanoBananaProMultiFunctionImageGenRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<NanoBananaProMultiFunctionImageGenResponse>, 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);
// 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(CommonWebError::NotAuthorized);
// }
//};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
let image_urls = match request.image_media_tokens.as_ref() {
Some(media_tokens) => {
info!("Looking up image media tokens: {:?}", media_tokens);
lookup_image_urls(
&http_request,
&mut mysql_connection,
server_state.server_environment,
media_tokens,
).await?
},
None => None,
};
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);
let fal_result;
if let Some(input_image_urls) = image_urls.as_deref() {
info!("nano banana pro edit image");
let num_images = match request.num_images {
Some(NanoBananaProMultiFunctionImageGenNumImages::One) => EnqueueNanoBananaProEditImageNumImages::One,
Some(NanoBananaProMultiFunctionImageGenNumImages::Two) => EnqueueNanoBananaProEditImageNumImages::Two,
Some(NanoBananaProMultiFunctionImageGenNumImages::Three) => EnqueueNanoBananaProEditImageNumImages::Three,
Some(NanoBananaProMultiFunctionImageGenNumImages::Four) => EnqueueNanoBananaProEditImageNumImages::Four,
None => EnqueueNanoBananaProEditImageNumImages::One, // Default to One
};
let resolution = match request.resolution {
Some(NanoBananaProMultiFunctionImageGenImageResolution::OneK) => EnqueueNanoBananaProEditImageResolution::OneK,
Some(NanoBananaProMultiFunctionImageGenImageResolution::TwoK) => EnqueueNanoBananaProEditImageResolution::TwoK,
Some(NanoBananaProMultiFunctionImageGenImageResolution::FourK) => EnqueueNanoBananaProEditImageResolution::FourK,
None => EnqueueNanoBananaProEditImageResolution::OneK,
};
let aspect_ratio = match request.aspect_ratio {
Some(NanoBananaProMultiFunctionImageGenAspectRatio::OneByOne) => EnqueueNanoBananaProEditImageAspectRatio::OneByOne,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::FiveByFour) => EnqueueNanoBananaProEditImageAspectRatio::FiveByFour,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::FourByThree) => EnqueueNanoBananaProEditImageAspectRatio::FourByThree,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::ThreeByTwo) => EnqueueNanoBananaProEditImageAspectRatio::ThreeByTwo,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::SixteenByNine) => EnqueueNanoBananaProEditImageAspectRatio::SixteenByNine,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::TwentyOneByNine) => EnqueueNanoBananaProEditImageAspectRatio::TwentyOneByNine,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::FourByFive) => EnqueueNanoBananaProEditImageAspectRatio::FourByFive,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::ThreeByFour) => EnqueueNanoBananaProEditImageAspectRatio::ThreeByFour,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::TwoByThree) => EnqueueNanoBananaProEditImageAspectRatio::TwoByThree,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::NineBySixteen) => EnqueueNanoBananaProEditImageAspectRatio::NineBySixteen,
None => EnqueueNanoBananaProEditImageAspectRatio::OneByOne,
};
let args = EnqueueNanoBananaProEditImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
image_urls: input_image_urls.to_owned(),
num_images,
resolution: Some(resolution),
aspect_ratio: Some(aspect_ratio),
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
fal_result = enqueue_nano_banana_pro_image_edit_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_nano_banana_pro_image_edit_webhook: {:?}", err);
CommonWebError::ServerError
})?;
} else {
info!("nano banana pro text-to-image");
let num_images = match request.num_images {
Some(NanoBananaProMultiFunctionImageGenNumImages::One) => EnqueueNanoBananaProTextToImageNumImages::One,
Some(NanoBananaProMultiFunctionImageGenNumImages::Two) => EnqueueNanoBananaProTextToImageNumImages::Two,
Some(NanoBananaProMultiFunctionImageGenNumImages::Three) => EnqueueNanoBananaProTextToImageNumImages::Three,
Some(NanoBananaProMultiFunctionImageGenNumImages::Four) => EnqueueNanoBananaProTextToImageNumImages::Four,
None => EnqueueNanoBananaProTextToImageNumImages::One, // Default to One
};
let resolution = match request.resolution {
Some(NanoBananaProMultiFunctionImageGenImageResolution::OneK) => EnqueueNanoBananaProTextToImageResolution::OneK,
Some(NanoBananaProMultiFunctionImageGenImageResolution::TwoK) => EnqueueNanoBananaProTextToImageResolution::TwoK,
Some(NanoBananaProMultiFunctionImageGenImageResolution::FourK) => EnqueueNanoBananaProTextToImageResolution::FourK,
None => EnqueueNanoBananaProTextToImageResolution::OneK,
};
let aspect_ratio = match request.aspect_ratio {
Some(NanoBananaProMultiFunctionImageGenAspectRatio::OneByOne) => EnqueueNanoBananaProTextToImageAspectRatio::OneByOne,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::FiveByFour) => EnqueueNanoBananaProTextToImageAspectRatio::FiveByFour,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::FourByThree) => EnqueueNanoBananaProTextToImageAspectRatio::FourByThree,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::ThreeByTwo) => EnqueueNanoBananaProTextToImageAspectRatio::ThreeByTwo,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::SixteenByNine) => EnqueueNanoBananaProTextToImageAspectRatio::SixteenByNine,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::TwentyOneByNine) => EnqueueNanoBananaProTextToImageAspectRatio::TwentyOneByNine,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::FourByFive) => EnqueueNanoBananaProTextToImageAspectRatio::FourByFive,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::ThreeByFour) => EnqueueNanoBananaProTextToImageAspectRatio::ThreeByFour,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::TwoByThree) => EnqueueNanoBananaProTextToImageAspectRatio::TwoByThree,
Some(NanoBananaProMultiFunctionImageGenAspectRatio::NineBySixteen) => EnqueueNanoBananaProTextToImageAspectRatio::NineBySixteen,
None => EnqueueNanoBananaProTextToImageAspectRatio::OneByOne,
};
let args = EnqueueNanoBananaProTextToImageArgs {
prompt: request.prompt.as_deref().unwrap_or(""),
num_images,
resolution: Some(resolution),
aspect_ratio: Some(aspect_ratio),
webhook_url: &server_state.fal.webhook_url,
api_key: &server_state.fal.api_key,
};
fal_result = enqueue_nano_banana_pro_text_to_image_webhook(args)
.await
.map_err(|err| {
warn!("Error calling enqueue_nano_banana_pro_text_to_image_webhook: {:?}", err);
CommonWebError::ServerError
})?;
}
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
})?;
// NB: Don't fail the job if the query fails.
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::NanoBananaPro),
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.
}
};
if let Some(media_tokens) = &request.image_media_tokens {
if let Some(token) = prompt_token.as_ref() {
let result = insert_batch_prompt_context_items(InsertBatchArgs {
prompt_token: token.clone(),
items: media_tokens.iter().map(|token| {
PromptContextItem {
media_token: token.clone(),
context_semantic_type: PromptContextSemanticType::Imgref,
}
}).collect(),
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::ImageGeneration,
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(NanoBananaProMultiFunctionImageGenResponse {
success: true,
inference_job_token: job_token,
}))
}
async fn lookup_image_urls(
http_request: &HttpRequest,
mysql_connection: &mut PoolConnection<MySql>,
server_environment: ServerEnvironment,
tokens: &[MediaFileToken],
) -> Result<Option<Vec<String>>, CommonWebError> {
const CAN_SEE_DELETED: bool = false;
let result = batch_get_media_files_by_tokens_with_connection(
mysql_connection,
tokens,
CAN_SEE_DELETED,
).await;
let media_files = match result {
Ok(files) => files,
Err(err) => {
error!("Error getting media files by tokens: {:?}", err);
return Err(CommonWebError::ServerError);
}
};
if media_files.len() != tokens.len() {
warn!("Wrong number of media files returned for tokens: {} found for {} tokens", media_files.len(), tokens.len());
let requested : HashSet<&MediaFileToken> = HashSet::from_iter(tokens.iter());
let returned : HashSet<&MediaFileToken> = HashSet::from_iter(media_files.iter().map(|m| &m.token));
let diff = requested.difference(&returned)
.cloned()
.collect::<Vec<&MediaFileToken>>();
return Err(CommonWebError::BadInputWithSimpleMessage(
format!("Not all media files could be found. Media files found: {}, tokens provided: {}, in original: {:?}, req {:?}, ret {:?}",
media_files.len(), tokens.len(), diff, requested, returned)));
}
let media_domain = get_media_domain(&http_request);
let image_urls = media_files.iter()
.map(|file| {
let public_bucket_path = MediaFileBucketPath::from_object_hash(
&file.public_bucket_directory_hash,
file.maybe_public_bucket_prefix.as_deref(),
file.maybe_public_bucket_extension.as_deref());
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_environment,
&public_bucket_path);
media_links.cdn_url.to_string()
})
.collect::<Vec<_>>();
if image_urls.is_empty() {
return Ok(None)
}
Ok(Some(image_urls))
}
@@ -10,6 +10,7 @@ use crate::http_server::endpoints::generate::image::generate_flux_pro_11_ultra_t
use crate::http_server::endpoints::generate::image::generate_gpt_image_1_text_to_image_handler::generate_gpt_image_1_text_to_image_handler;
use crate::http_server::endpoints::generate::image::inpaint::flux_dev_juggernaut_inpaint_handler::flux_dev_juggernaut_inpaint_image_handler;
use crate::http_server::endpoints::generate::image::inpaint::flux_pro_1_inpaint_handler::flux_pro_1_inpaint_image_handler;
use crate::http_server::endpoints::generate::image::multi_function::nano_banana_pro_multi_function_image_gen_handler::nano_banana_pro_multi_function_image_gen_handler;
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;
@@ -39,6 +40,12 @@ where
{
app.service(web::scope("/v1/generate")
.service(web::scope("/image")
.service(web::scope("/multi_function")
.service(web::resource("/nano_banana_pro")
.route(web::post().to(nano_banana_pro_multi_function_image_gen_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
.service(web::scope("/edit")
.service(web::resource("/flux_pro_kontext_max")
.route(web::post().to(flux_pro_kontext_max_edit_image_handler))