mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-30 15:25:57 -04:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
160dbcf91d | ||
|
|
cb7c854d9e | ||
|
|
de6a9a25cd | ||
|
|
6231894eff | ||
|
|
5d7d9ad03c |
+116
@@ -0,0 +1,116 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_image_model::CommonImageModel;
|
||||
use enums::common::generation::common_video_resolution::CommonVideoResolution;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
|
||||
pub const ESTIMATE_IMAGE_COST_PATH: &str = "/v1/generate/cost_estimate/image";
|
||||
|
||||
/// Request body for the image cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateImageCostRequest {
|
||||
/// The image model to estimate costs for.
|
||||
pub model: CommonImageModel,
|
||||
|
||||
/// The provider to route the generation through.
|
||||
pub provider: GenerationProvider,
|
||||
|
||||
/// The type of generation (determines whether input images are involved).
|
||||
/// This is a tagged enum, so it looks like:
|
||||
/// "generation_mode": {"type": "text_to_image"}
|
||||
/// "generation_mode": {"type": "image_edit", "count": 2}
|
||||
pub generation_mode: GenerationMode,
|
||||
|
||||
/// Optional aspect ratio.
|
||||
pub aspect_ratio: Option<CommonAspectRatio>,
|
||||
|
||||
/// Optional resolution.
|
||||
pub resolution: Option<CommonVideoResolution>,
|
||||
|
||||
/// Number of images to generate in parallel.
|
||||
pub image_batch_count: Option<u16>,
|
||||
}
|
||||
|
||||
/// Describes the type of image generation being requested.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum GenerationMode {
|
||||
/// No input images — generate from prompt only.
|
||||
TextToImage,
|
||||
/// One or more input images are provided for editing/compositing.
|
||||
ImageEdit { count: u32 },
|
||||
}
|
||||
|
||||
/// Response body for the image cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateImageCostResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// Estimated cost in credits.
|
||||
pub cost_in_credits: Option<u64>,
|
||||
|
||||
/// Estimated cost in USD cents.
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
|
||||
/// Whether the generation is free for this user/plan.
|
||||
pub is_free: bool,
|
||||
|
||||
/// Whether the user has unlimited generations.
|
||||
pub is_unlimited: bool,
|
||||
|
||||
/// Whether the user is rate limited.
|
||||
pub is_rate_limited: bool,
|
||||
|
||||
/// Whether the output will have a watermark.
|
||||
pub has_watermark: bool,
|
||||
}
|
||||
|
||||
/// Error response for the image cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateImageCostError {
|
||||
pub success: bool,
|
||||
pub error_type: EstimateImageCostErrorType,
|
||||
pub error_message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EstimateImageCostErrorType {
|
||||
InvalidProviderForModel,
|
||||
InvalidInput,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization_text_to_image() {
|
||||
let request = EstimateImageCostRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
provider: GenerationProvider::Artcraft,
|
||||
generation_mode: GenerationMode::TextToImage,
|
||||
aspect_ratio: None,
|
||||
resolution: None,
|
||||
image_batch_count: None,
|
||||
};
|
||||
let serialized = serde_json::to_string(&request).unwrap();
|
||||
assert!(serialized.contains("\"generation_mode\":{\"type\":\"text_to_image\"}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialization_image_edit() {
|
||||
let request = EstimateImageCostRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
provider: GenerationProvider::Artcraft,
|
||||
generation_mode: GenerationMode::ImageEdit { count: 2 },
|
||||
aspect_ratio: None,
|
||||
resolution: None,
|
||||
image_batch_count: None,
|
||||
};
|
||||
let serialized = serde_json::to_string(&request).unwrap();
|
||||
assert!(serialized.contains("\"generation_mode\":{\"type\":\"image_edit\",\"count\":2}"));
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod estimate_image_cost;
|
||||
pub mod estimate_video_cost;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Common image models supported by the router.
|
||||
/// Not all models are available through all providers.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonImageModel {
|
||||
#[serde(rename = "nano_banana_pro")]
|
||||
NanaBananaPro,
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use serde_derive::{Deserialize, Serialize};
|
||||
/// In the case a model doesn't support the resolution, gracefully pick the nearest option.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonVideoResolution {
|
||||
pub enum CommonResolution {
|
||||
/// Models: Nano Banana Pro
|
||||
OneK,
|
||||
/// Models: Nano Banana Pro
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod common_aspect_ratio;
|
||||
pub mod common_image_model;
|
||||
pub mod common_resolution;
|
||||
pub mod common_video_model;
|
||||
pub mod image_list_ref;
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{
|
||||
NanoBananaProMultiFunctionImageGenImageResolution, NanoBananaProMultiFunctionImageGenNumImages,
|
||||
};
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::PlanArtcraftNanaBananaPro;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_nano_banana_pro(
|
||||
plan: &PlanArtcraftNanaBananaPro<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.15/image at 1K or 2K resolution, $0.30/image at 4K.
|
||||
// (Stored as USD cents.)
|
||||
let cost_per_image: u64 = match plan.resolution {
|
||||
Some(NanoBananaProMultiFunctionImageGenImageResolution::FourK) => 30,
|
||||
_ => 15,
|
||||
};
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
NanoBananaProMultiFunctionImageGenNumImages::One => 1,
|
||||
NanoBananaProMultiFunctionImageGenNumImages::Two => 2,
|
||||
NanoBananaProMultiFunctionImageGenNumImages::Three => 3,
|
||||
NanoBananaProMultiFunctionImageGenNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
|
||||
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution,
|
||||
aspect_ratio: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_usd_cents() {
|
||||
// 1K/2K: $0.15/image = 15 cents
|
||||
assert_eq!(estimate_usd_cents(None, 1), 15);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 1), 15);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 1), 15);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 2), 30);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 3), 45);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 4), 60);
|
||||
|
||||
// 4K: $0.30/image = 30 cents
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 1), 30);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 2), 60);
|
||||
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 4), 120);
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod estimate_image_cost_artcraft_nano_banana_pro;
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod artcraft;
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::image_generation_plan::GenerateImageResponse;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::PlanArtcraftNanaBananaPro;
|
||||
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::NanoBananaProMultiFunctionImageGenRequest;
|
||||
use artcraft_client::endpoints::generate::image::multi_function::nano_banana_pro_multi_function_image_gen_image::nano_banana_pro_multi_function_image_gen;
|
||||
|
||||
pub async fn execute_artcraft_nano_banana_pro(
|
||||
plan: &PlanArtcraftNanaBananaPro<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = NanoBananaProMultiFunctionImageGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
|
||||
num_images: Some(plan.num_images),
|
||||
resolution: plan.resolution,
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
};
|
||||
|
||||
let response = nano_banana_pro_multi_function_image_gen(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateImageResponse {
|
||||
inference_job_token: response.inference_job_token,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::{base_image_request, get_artcraft_client};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_image_nano_banana_pro() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_image_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
println!("Job token: {:?}", response.inference_job_token);
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_image_nano_banana_pro_batch_four() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_image_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
println!("Job token: {:?}", response.inference_job_token);
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_edit_image_nano_banana_pro() {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
// Reference images: the man to transplant, then the destination scene.
|
||||
let image_tokens = vec![
|
||||
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
|
||||
MediaFileToken::new_from_str("m_2220d5we3masng5hvzg5zz68ywbdqd"),
|
||||
];
|
||||
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Add the man in the first image into the fantasy scene in the second image. Keep his hoodie and attire, but match the vibe of the fantasy scene."),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
image_batch_count: Some(1),
|
||||
..base_image_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
println!("Job token: {:?}", response.inference_job_token);
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod generate_image_artcraft_nano_banana_pro;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod artcraft;
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::plan_generate_image_artcraft_nano_banana_pro;
|
||||
|
||||
pub struct GenerateImageRequest<'a> {
|
||||
/// Which model to use.
|
||||
pub model: CommonImageModel,
|
||||
|
||||
/// Which provider to use.
|
||||
pub provider: Provider,
|
||||
|
||||
/// The prompt for the image generation.
|
||||
pub prompt: Option<&'a str>,
|
||||
|
||||
/// Input images for image editing.
|
||||
/// If present, we're doing image editing (image-to-image).
|
||||
/// If absent, we're doing text-to-image.
|
||||
pub image_inputs: Option<ImageListRef<'a>>,
|
||||
|
||||
/// The resolution to use.
|
||||
pub resolution: Option<CommonResolution>,
|
||||
|
||||
/// The aspect ratio to use.
|
||||
pub aspect_ratio: Option<CommonAspectRatio>,
|
||||
|
||||
/// How many images to generate.
|
||||
pub image_batch_count: Option<u16>,
|
||||
|
||||
/// If the request is a mismatch with the (model/provider), how to mitigate it.
|
||||
pub request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy,
|
||||
|
||||
/// Some providers support idempotency.
|
||||
/// If not supplied, we'll generate one for the required providers.
|
||||
pub idempotency_token: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> GenerateImageRequest<'a> {
|
||||
/// Read the image generation request, construct a plan, then yield a means to execute it.
|
||||
pub fn build(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.provider {
|
||||
Provider::Artcraft => match self.model {
|
||||
CommonImageModel::NanaBananaPro => {
|
||||
plan_generate_image_artcraft_nano_banana_pro(self).map(ImageGenerationPlan::ArtcraftNanaBananaPro)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_generate_idempotency_token(&self) -> String {
|
||||
self.idempotency_token.map(|t| t.to_string())
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/// The result of estimating the cost of an image generation plan.
|
||||
pub struct ImageGenerationCostEstimate {
|
||||
pub cost_in_credits: Option<u64>,
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
pub is_free: bool,
|
||||
pub is_unlimited: bool,
|
||||
pub is_rate_limited: bool,
|
||||
pub has_watermark: bool,
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana_pro::estimate_image_cost_artcraft_nano_banana_pro;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana_pro::execute_artcraft_nano_banana_pro;
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::PlanArtcraftNanaBananaPro;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateImageResponse {
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ImageGenerationPlan<'a> {
|
||||
ArtcraftNanaBananaPro(PlanArtcraftNanaBananaPro<'a>),
|
||||
}
|
||||
|
||||
impl<'a> ImageGenerationPlan<'a> {
|
||||
pub async fn generate_image(
|
||||
&self,
|
||||
client: &RouterClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
ImageGenerationPlan::ArtcraftNanaBananaPro(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_nano_banana_pro(plan, artcraft_client).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_costs(&self) -> ImageGenerationCostEstimate {
|
||||
match self {
|
||||
ImageGenerationPlan::ArtcraftNanaBananaPro(plan) => {
|
||||
estimate_image_cost_artcraft_nano_banana_pro(plan)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod cost;
|
||||
pub mod execute;
|
||||
pub mod generate_image_request;
|
||||
pub mod image_generation_cost_estimate;
|
||||
pub mod image_generation_plan;
|
||||
pub mod plan;
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod plan_generate_image_artcraft_nano_banana_pro;
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{
|
||||
NanoBananaProMultiFunctionImageGenAspectRatio, NanoBananaProMultiFunctionImageGenImageResolution,
|
||||
NanoBananaProMultiFunctionImageGenNumImages,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftNanaBananaPro<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
/// Input images for image editing. None means text-to-image mode.
|
||||
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
|
||||
pub aspect_ratio: Option<NanoBananaProMultiFunctionImageGenAspectRatio>,
|
||||
pub resolution: Option<NanoBananaProMultiFunctionImageGenImageResolution>,
|
||||
pub num_images: NanoBananaProMultiFunctionImageGenNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_nano_banana_pro<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftNanaBananaPro<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftNanaBananaPro {
|
||||
prompt: request.prompt,
|
||||
image_inputs,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nano Banana Pro supported aspect ratios:
|
||||
// Text-to-image: OneByOne, FiveByFour, FourByThree, ThreeByTwo, SixteenByNine, TwentyOneByNine,
|
||||
// FourByFive, ThreeByFour, TwoByThree, NineBySixteen
|
||||
// Image editing: Auto (uses source image AR), plus all text-to-image ratios
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
is_edit_mode: bool,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<NanoBananaProMultiFunctionImageGenAspectRatio>, ArtcraftRouterError> {
|
||||
use NanoBananaProMultiFunctionImageGenAspectRatio as NbpAr;
|
||||
match aspect_ratio {
|
||||
// No preference — let the model use its default
|
||||
None => Ok(None),
|
||||
|
||||
// Auto: only valid in edit mode (use source image dimensions)
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => {
|
||||
if is_edit_mode {
|
||||
Ok(Some(NbpAr::Auto))
|
||||
} else {
|
||||
// Auto not valid for text-to-image; fall back to square default
|
||||
Ok(Some(NbpAr::OneByOne))
|
||||
}
|
||||
}
|
||||
|
||||
// Direct mappings
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(NbpAr::OneByOne)),
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(NbpAr::FiveByFour)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(NbpAr::FourByThree)),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(NbpAr::ThreeByTwo)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(NbpAr::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(NbpAr::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(NbpAr::FourByFive)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(NbpAr::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(NbpAr::TwoByThree)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(NbpAr::NineBySixteen)),
|
||||
|
||||
// Mismatch — TallNineByTwentyOne has no direct equivalent
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(nearest_aspect_ratio(unsupported)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick the nearest supported aspect ratio for unsupported inputs.
|
||||
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> NanoBananaProMultiFunctionImageGenAspectRatio {
|
||||
use NanoBananaProMultiFunctionImageGenAspectRatio as NbpAr;
|
||||
match aspect_ratio {
|
||||
// TallNineByTwentyOne (0.43) — nearest is NineBySixteen (0.5625)
|
||||
CommonAspectRatio::TallNineByTwentyOne => NbpAr::NineBySixteen,
|
||||
_ => NbpAr::OneByOne,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
_strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<NanoBananaProMultiFunctionImageGenImageResolution>, ArtcraftRouterError> {
|
||||
use CommonResolution as R;
|
||||
use NanoBananaProMultiFunctionImageGenImageResolution as NbpRes;
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
Some(R::OneK) => Ok(Some(NbpRes::OneK)),
|
||||
Some(R::TwoK) => Ok(Some(NbpRes::TwoK)),
|
||||
Some(R::FourK) => Ok(Some(NbpRes::FourK)),
|
||||
Some(R::ThreeK) => {
|
||||
// No 3K option; default to 2K
|
||||
Ok(Some(NbpRes::TwoK))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nano Banana Pro supports 1, 2, 3, and 4 images.
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<NanoBananaProMultiFunctionImageGenNumImages, ArtcraftRouterError> {
|
||||
use NanoBananaProMultiFunctionImageGenNumImages as NbpN;
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(NbpN::One),
|
||||
2 => Ok(NbpN::Two),
|
||||
3 => Ok(NbpN::Three),
|
||||
4 => Ok(NbpN::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(NbpN::Four),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(NbpN::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_image_request;
|
||||
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{
|
||||
NanoBananaProMultiFunctionImageGenAspectRatio as NbpAr,
|
||||
NanoBananaProMultiFunctionImageGenImageResolution as NbpRes,
|
||||
NanoBananaProMultiFunctionImageGenNumImages as NbpN,
|
||||
};
|
||||
|
||||
// ── Aspect ratio ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_none_is_none() {
|
||||
let request = GenerateImageRequest { aspect_ratio: None, ..base_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(plan.aspect_ratio.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_square() {
|
||||
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
|
||||
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(NbpAr::OneByOne)), "expected OneByOne for {:?}", ar);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_wide() {
|
||||
let cases = [
|
||||
(CommonAspectRatio::WideFiveByFour, NbpAr::FiveByFour),
|
||||
(CommonAspectRatio::WideFourByThree, NbpAr::FourByThree),
|
||||
(CommonAspectRatio::WideThreeByTwo, NbpAr::ThreeByTwo),
|
||||
(CommonAspectRatio::WideSixteenByNine, NbpAr::SixteenByNine),
|
||||
(CommonAspectRatio::Wide, NbpAr::SixteenByNine),
|
||||
(CommonAspectRatio::WideTwentyOneByNine, NbpAr::TwentyOneByNine),
|
||||
];
|
||||
for (common, expected) in cases {
|
||||
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(
|
||||
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
|
||||
"expected {:?} for {:?}", expected, common,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_tall() {
|
||||
let cases = [
|
||||
(CommonAspectRatio::TallFourByFive, NbpAr::FourByFive),
|
||||
(CommonAspectRatio::TallThreeByFour, NbpAr::ThreeByFour),
|
||||
(CommonAspectRatio::TallTwoByThree, NbpAr::TwoByThree),
|
||||
(CommonAspectRatio::TallNineBySixteen, NbpAr::NineBySixteen),
|
||||
(CommonAspectRatio::Tall, NbpAr::NineBySixteen),
|
||||
];
|
||||
for (common, expected) in cases {
|
||||
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(
|
||||
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
|
||||
"expected {:?} for {:?}", expected, common,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_auto_in_edit_mode_yields_auto() {
|
||||
// Auto is only valid for edit mode (image_inputs present)
|
||||
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(auto_ar),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(
|
||||
matches!(plan.aspect_ratio, Some(NbpAr::Auto)),
|
||||
"expected Auto in edit mode for {:?}", auto_ar,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_auto_in_text_to_image_mode_falls_back_to_square() {
|
||||
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(auto_ar),
|
||||
image_inputs: None,
|
||||
..base_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(
|
||||
matches!(plan.aspect_ratio, Some(NbpAr::OneByOne)),
|
||||
"expected OneByOne fallback in text-to-image for {:?}", auto_ar,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_unsupported_error_out() {
|
||||
// TallNineByTwentyOne has no direct equivalent
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_unsupported_nearest_match() {
|
||||
// TallNineByTwentyOne (0.43) is nearest to NineBySixteen (0.5625)
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(
|
||||
matches!(plan.aspect_ratio, Some(NbpAr::NineBySixteen)),
|
||||
"expected NineBySixteen nearest match with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resolution ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn resolution_none_is_none() {
|
||||
let request = GenerateImageRequest { resolution: None, ..base_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(plan.resolution.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_direct_mappings() {
|
||||
let cases = [
|
||||
(CommonResolution::OneK, NbpRes::OneK),
|
||||
(CommonResolution::TwoK, NbpRes::TwoK),
|
||||
(CommonResolution::FourK, NbpRes::FourK),
|
||||
];
|
||||
for (common, expected) in cases {
|
||||
let request = GenerateImageRequest { resolution: Some(common), ..base_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(
|
||||
matches!(plan.resolution, Some(r) if std::mem::discriminant(&r) == std::mem::discriminant(&expected)),
|
||||
"expected {:?} for {:?}", expected, common,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_three_k_falls_back_to_two_k() {
|
||||
let request = GenerateImageRequest {
|
||||
resolution: Some(CommonResolution::ThreeK),
|
||||
..base_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.resolution, Some(NbpRes::TwoK)));
|
||||
}
|
||||
|
||||
// ── Num images ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn num_images_zero_is_always_error() {
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::ErrorOut,
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(
|
||||
matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
|
||||
"expected UserRequestedZeroGenerations with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_direct_mapping() {
|
||||
let cases = [
|
||||
(1, NbpN::One),
|
||||
(2, NbpN::Two),
|
||||
(3, NbpN::Three),
|
||||
(4, NbpN::Four),
|
||||
];
|
||||
for (count, expected) in cases {
|
||||
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(
|
||||
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
|
||||
"expected {:?} for count {}", expected, count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_out_of_range_error_out() {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_out_of_range_clamps_to_four() {
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(
|
||||
matches!(plan.num_images, NbpN::Four),
|
||||
"expected Four for count 5 with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Image inputs ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn url_image_inputs_returns_error() {
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_image_inputs_is_text_to_image_mode() {
|
||||
let request = GenerateImageRequest { image_inputs: None, ..base_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(plan.image_inputs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_token_image_inputs_is_edit_mode() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap();
|
||||
assert!(plan.image_inputs.is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod artcraft;
|
||||
+53
@@ -34,3 +34,56 @@ pub async fn execute_artcraft_seedance2p0(
|
||||
all_inference_job_tokens: response.all_inference_job_tokens,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use crate::test_helpers::{base_video_request, get_artcraft_client};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_video_seedance_2p0() {
|
||||
let client = get_artcraft_client();
|
||||
let request = crate::generate::generate_video::generate_video_request::GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
video_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_video_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_video(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_video request failed");
|
||||
println!("Job token: {:?}", response.inference_job_token);
|
||||
println!("All job tokens: {:?}", response.all_inference_job_tokens);
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_video_seedance_2p0_batch_two() {
|
||||
let client = get_artcraft_client();
|
||||
let request = crate::generate::generate_video::generate_video_request::GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
video_batch_count: Some(2),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_video_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_video(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_video request failed");
|
||||
println!("Job tokens ({} total):", response.all_inference_job_tokens.len());
|
||||
for token in &response.all_inference_job_tokens {
|
||||
println!(" {:?}", token);
|
||||
}
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonVideoResolution;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
@@ -32,7 +32,7 @@ pub struct GenerateVideoRequest<'a> {
|
||||
pub reference_images: Option<ImageListRef<'a>>,
|
||||
|
||||
/// The resolution to use
|
||||
pub resolution: Option<CommonVideoResolution>,
|
||||
pub resolution: Option<CommonResolution>,
|
||||
|
||||
/// The aspect ratio to use
|
||||
pub aspect_ratio: Option<CommonAspectRatio>,
|
||||
|
||||
+149
@@ -161,6 +161,7 @@ fn plan_batch_count(
|
||||
}
|
||||
|
||||
// Seedance2p0 supports duration of 4–15 seconds.
|
||||
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
@@ -181,3 +182,151 @@ fn plan_duration(
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use crate::test_helpers::base_video_request;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
Seedance2p0AspectRatio, Seedance2p0BatchCount,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_16x9() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_video_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Landscape16x9)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_9x16() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_video_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Portrait9x16)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_square() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
..base_video_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Square1x1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_nearest_match_both_strategies() {
|
||||
// WideThreeByTwo (AR 1.5) is nearest to Standard4x3 (1.33), not Landscape16x9 (1.78)
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_video_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(
|
||||
matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Standard4x3)),
|
||||
"Expected Standard4x3 for WideThreeByTwo with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_error_out_on_unsupported() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_video_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_zero_is_always_error() {
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::ErrorOut,
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateVideoRequest {
|
||||
video_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_video_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(
|
||||
matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
|
||||
"Expected UserRequestedZeroGenerations with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_direct_mapping() {
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(1), ..base_video_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::One));
|
||||
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(2), ..base_video_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Two));
|
||||
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(4), ..base_video_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_three_upgrade_rounds_to_four() {
|
||||
let request = GenerateVideoRequest {
|
||||
video_batch_count: Some(3),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_video_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_three_downgrade_rounds_to_two() {
|
||||
let request = GenerateVideoRequest {
|
||||
video_batch_count: Some(3),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
..base_video_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Two));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_image_ref_returns_error() {
|
||||
let request = GenerateVideoRequest {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/image.jpg")),
|
||||
..base_video_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod generate_image;
|
||||
pub mod generate_video;
|
||||
|
||||
@@ -2,3 +2,6 @@ pub mod api;
|
||||
pub mod client;
|
||||
pub mod errors;
|
||||
pub mod generate;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_helpers;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
|
||||
pub fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read /Users/bt/Artcraft/credentials/artcraft_cookies.txt");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
pub fn base_image_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::NanaBananaPro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
image_batch_count: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base_video_request() -> GenerateVideoRequest<'static> {
|
||||
GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel;
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use artcraft_router::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use artcraft_router::client::router_client::RouterClient;
|
||||
use artcraft_router::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use artcraft_router::errors::client_error::ClientError;
|
||||
use artcraft_router::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use artcraft_router::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
Seedance2p0AspectRatio, Seedance2p0BatchCount,
|
||||
};
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::credentials::storyteller_session_cookie::StorytellerSessionCookie;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
|
||||
fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read /Users/bt/Artcraft/credentials/artcraft_cookies.txt");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
fn base_request() -> GenerateVideoRequest<'static> {
|
||||
GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space"),
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
mod plan_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_16x9() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Landscape16x9)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_9x16() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Portrait9x16)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_square() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Square1x1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_nearest_match_both_strategies() {
|
||||
// WideThreeByTwo (AR 1.5) is nearest to Standard4x3 (1.33), not Landscape16x9 (1.78)
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(
|
||||
matches!(plan.aspect_ratio, Some(Seedance2p0AspectRatio::Standard4x3)),
|
||||
"Expected Standard4x3 for WideThreeByTwo with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_error_out_on_unsupported() {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_zero_is_always_error() {
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::ErrorOut,
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateVideoRequest {
|
||||
video_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(
|
||||
matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
|
||||
"Expected UserRequestedZeroGenerations with {:?}", strategy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_direct_mapping() {
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(1), ..base_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::One));
|
||||
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(2), ..base_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Two));
|
||||
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(4), ..base_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_three_upgrade_rounds_to_four() {
|
||||
let request = GenerateVideoRequest {
|
||||
video_batch_count: Some(3),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_three_downgrade_rounds_to_two() {
|
||||
let request = GenerateVideoRequest {
|
||||
video_batch_count: Some(3),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
..base_request()
|
||||
};
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = request.build().unwrap();
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Two));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_image_ref_returns_error() {
|
||||
let request = GenerateVideoRequest {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/image.jpg")),
|
||||
..base_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
mod real_requests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_video_seedance_2p0() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
video_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
..base_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_video(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_video request failed");
|
||||
println!("Job token: {:?}", response.inference_job_token);
|
||||
println!("All job tokens: {:?}", response.all_inference_job_tokens);
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_video_seedance_2p0_batch_two() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
video_batch_count: Some(2),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
..base_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_video(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_video request failed");
|
||||
println!("Job tokens ({} total):", response.all_inference_job_tokens.len());
|
||||
for token in &response.all_inference_job_tokens {
|
||||
println!(" {:?}", token);
|
||||
}
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Image models available for generation.
|
||||
/// Mirrors artcraft_router::api::common_image_model::CommonImageModel.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonImageModel {
|
||||
#[serde(rename = "nano_banana_pro")]
|
||||
NanaBananaPro,
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Common video resolutions.
|
||||
/// Mirrors artcraft_router::api::common_resolution::CommonVideoResolution.
|
||||
/// Mirrors artcraft_router::api::common_resolution::CommonResolution.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonVideoResolution {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod common_aspect_ratio;
|
||||
pub mod common_image_model;
|
||||
pub mod common_video_model;
|
||||
pub mod common_video_resolution;
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use artcraft_api_defs::generate::cost_estimate::estimate_image_cost::{
|
||||
EstimateImageCostError, EstimateImageCostErrorType, EstimateImageCostRequest,
|
||||
EstimateImageCostResponse,
|
||||
};
|
||||
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterAspectRatio;
|
||||
use artcraft_router::api::common_image_model::CommonImageModel as RouterImageModel;
|
||||
use artcraft_router::api::common_resolution::CommonResolution as RouterResolution;
|
||||
use artcraft_router::api::provider::Provider as RouterProvider;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_image_model::CommonImageModel;
|
||||
use enums::common::generation::common_video_resolution::CommonVideoResolution;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
|
||||
/// Estimate the credit and USD cost of an image generation request.
|
||||
/// Does not require authentication and does not charge any credits.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Cost Estimate",
|
||||
path = "/v1/generate/cost_estimate/image",
|
||||
responses(
|
||||
(status = 200, description = "Cost estimate", body = EstimateImageCostResponse),
|
||||
(status = 400, description = "Invalid request", body = EstimateImageCostError),
|
||||
),
|
||||
)]
|
||||
pub async fn estimate_image_cost_handler(
|
||||
request: Json<EstimateImageCostRequest>,
|
||||
) -> Result<Json<EstimateImageCostResponse>, HandlerError> {
|
||||
let router_provider = map_provider(request.provider, request.model)?;
|
||||
let router_model = map_image_model(request.model)?;
|
||||
let router_aspect_ratio = request.aspect_ratio.map(map_aspect_ratio);
|
||||
let router_resolution = request.resolution.map(map_resolution);
|
||||
|
||||
let router_request = GenerateImageRequest {
|
||||
model: router_model,
|
||||
provider: router_provider,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: router_resolution,
|
||||
aspect_ratio: router_aspect_ratio,
|
||||
image_batch_count: request.image_batch_count,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
idempotency_token: None,
|
||||
};
|
||||
|
||||
let plan = router_request.build()
|
||||
.map_err(|e| HandlerError::InvalidInput(format!("{}", e)))?;
|
||||
|
||||
let estimate = plan.estimate_costs();
|
||||
|
||||
Ok(Json(EstimateImageCostResponse {
|
||||
success: true,
|
||||
cost_in_credits: estimate.cost_in_credits,
|
||||
cost_in_usd_cents: estimate.cost_in_usd_cents,
|
||||
is_free: estimate.is_free,
|
||||
is_unlimited: estimate.is_unlimited,
|
||||
is_rate_limited: estimate.is_rate_limited,
|
||||
has_watermark: estimate.has_watermark,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Local error type — wraps the serializable API error struct so we can implement
|
||||
/// ResponseError (orphan rules prevent implementing it directly on the foreign type).
|
||||
#[derive(Debug)]
|
||||
pub enum HandlerError {
|
||||
InvalidProviderForModel { provider: String, model: String },
|
||||
InvalidInput(String),
|
||||
}
|
||||
|
||||
impl std::error::Error for HandlerError {}
|
||||
|
||||
impl Display for HandlerError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for HandlerError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let (error_type, error_message) = match self {
|
||||
HandlerError::InvalidProviderForModel { provider, model } => (
|
||||
EstimateImageCostErrorType::InvalidProviderForModel,
|
||||
format!("Provider '{}' is not supported for model '{}'", provider, model),
|
||||
),
|
||||
HandlerError::InvalidInput(msg) => (
|
||||
EstimateImageCostErrorType::InvalidInput,
|
||||
msg.clone(),
|
||||
),
|
||||
};
|
||||
HttpResponse::BadRequest().json(EstimateImageCostError {
|
||||
success: false,
|
||||
error_type,
|
||||
error_message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn map_provider(
|
||||
provider: GenerationProvider,
|
||||
model: CommonImageModel,
|
||||
) -> Result<RouterProvider, HandlerError> {
|
||||
match provider {
|
||||
GenerationProvider::Artcraft => Ok(RouterProvider::Artcraft),
|
||||
other => Err(HandlerError::InvalidProviderForModel {
|
||||
provider: format!("{:?}", other),
|
||||
model: format!("{:?}", model),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_image_model(model: CommonImageModel) -> Result<RouterImageModel, HandlerError> {
|
||||
let router_model = match model {
|
||||
CommonImageModel::NanaBananaPro => RouterImageModel::NanaBananaPro,
|
||||
};
|
||||
Ok(router_model)
|
||||
}
|
||||
|
||||
fn map_aspect_ratio(ar: CommonAspectRatio) -> RouterAspectRatio {
|
||||
match ar {
|
||||
CommonAspectRatio::Auto => RouterAspectRatio::Auto,
|
||||
CommonAspectRatio::Square => RouterAspectRatio::Square,
|
||||
CommonAspectRatio::WideThreeByTwo => RouterAspectRatio::WideThreeByTwo,
|
||||
CommonAspectRatio::WideFourByThree => RouterAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideFiveByFour => RouterAspectRatio::WideFiveByFour,
|
||||
CommonAspectRatio::WideSixteenByNine => RouterAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideTwentyOneByNine => RouterAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::TallTwoByThree => RouterAspectRatio::TallTwoByThree,
|
||||
CommonAspectRatio::TallThreeByFour => RouterAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallFourByFive => RouterAspectRatio::TallFourByFive,
|
||||
CommonAspectRatio::TallNineBySixteen => RouterAspectRatio::TallNineBySixteen,
|
||||
CommonAspectRatio::TallNineByTwentyOne => RouterAspectRatio::TallNineByTwentyOne,
|
||||
CommonAspectRatio::Wide => RouterAspectRatio::Wide,
|
||||
CommonAspectRatio::Tall => RouterAspectRatio::Tall,
|
||||
CommonAspectRatio::Auto2k => RouterAspectRatio::Auto2k,
|
||||
CommonAspectRatio::Auto4k => RouterAspectRatio::Auto4k,
|
||||
CommonAspectRatio::SquareHd => RouterAspectRatio::SquareHd,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_resolution(res: CommonVideoResolution) -> RouterResolution {
|
||||
match res {
|
||||
CommonVideoResolution::OneK => RouterResolution::OneK,
|
||||
CommonVideoResolution::TwoK => RouterResolution::TwoK,
|
||||
CommonVideoResolution::ThreeK => RouterResolution::ThreeK,
|
||||
CommonVideoResolution::FourK => RouterResolution::FourK,
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod estimate_image_cost_handler;
|
||||
+1
@@ -1 +1,2 @@
|
||||
pub mod image;
|
||||
pub mod video;
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ use artcraft_api_defs::generate::cost_estimate::estimate_video_cost::{
|
||||
EstimateVideoCostResponse,
|
||||
};
|
||||
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterAspectRatio;
|
||||
use artcraft_router::api::common_resolution::CommonVideoResolution as RouterResolution;
|
||||
use artcraft_router::api::common_resolution::CommonResolution as RouterResolution;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel as RouterVideoModel;
|
||||
use artcraft_router::api::provider::Provider as RouterProvider;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
|
||||
+5
@@ -1,3 +1,4 @@
|
||||
use crate::http_server::endpoints::generate::cost_estimate::image::estimate_image_cost_handler::estimate_image_cost_handler;
|
||||
use crate::http_server::endpoints::generate::cost_estimate::video::estimate_video_cost_handler::estimate_video_cost_handler;
|
||||
use crate::http_server::endpoints::generate::image::edit::flux_pro_kontext_max_edit_image_handler::flux_pro_kontext_max_edit_image_handler;
|
||||
use crate::http_server::endpoints::generate::image::edit::gemini_25_flash_edit_image_handler::gemini_25_flash_edit_image_handler;
|
||||
@@ -53,6 +54,10 @@ where
|
||||
{
|
||||
app.service(web::scope("/v1/generate")
|
||||
.service(web::scope("/cost_estimate")
|
||||
.service(web::resource("/image")
|
||||
.route(web::post().to(estimate_image_cost_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/video")
|
||||
.route(web::post().to(estimate_video_cost_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
|
||||
Reference in New Issue
Block a user