mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 15:37:49 +00:00
mesh and splat omni endpoints (#1752)
This commit is contained in:
1 parent
9cf1b8e066
commit
5bda2a6cb4
212 files changed
+10676
-15
No files matched your search
+2
@@ -1,3 +1,5 @@
|
||||
pub mod omni_gen_audio_cost_and_generate_request;
|
||||
pub mod omni_gen_image_cost_and_generate_request;
|
||||
pub mod omni_gen_mesh_cost_and_generate_request;
|
||||
pub mod omni_gen_splat_cost_and_generate_request;
|
||||
pub mod omni_gen_video_cost_and_generate_request;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel;
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
use enums::common::generation::common_polygon_type::CommonPolygonType;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Shared request body for both the mesh cost estimate and mesh generation endpoints.
|
||||
#[derive(Clone, Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct OmniGenMeshCostAndGenerateRequest {
|
||||
/// REQUIRED (even if marked optional)
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub idempotency_token: Option<String>,
|
||||
|
||||
/// REQUIRED (even if marked optional)
|
||||
/// Which model to use.
|
||||
pub model: Option<CommonMeshModel>,
|
||||
|
||||
/// The text prompt for the mesh generation (text-to-3D).
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Reference images (optional).
|
||||
/// The primary/front input image, or the sketch image for sketch-to-3D.
|
||||
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Front view image (optional; multi-view input).
|
||||
/// Can be used instead of `reference_image_media_tokens[0]` when the other
|
||||
/// side views are supplied.
|
||||
pub front_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Back view image (optional; multi-view input).
|
||||
pub back_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Left view image (optional; multi-view input).
|
||||
pub left_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Right view image (optional; multi-view input).
|
||||
pub right_image_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// The mesh output type to generate (normal, low-poly, or geometry-only).
|
||||
pub mesh_output_type: Option<CommonMeshOutputType>,
|
||||
|
||||
/// The polygon type to use for the generated mesh (triangle or quad).
|
||||
pub polygon_type: Option<CommonPolygonType>,
|
||||
|
||||
/// Target face count for the generated mesh.
|
||||
pub face_count: Option<u64>,
|
||||
|
||||
/// Whether to generate PBR (physically based rendering) materials.
|
||||
pub enable_pbr: Option<bool>,
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Shared request body for both the splat cost estimate and splat generation endpoints.
|
||||
#[derive(Clone, Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct OmniGenSplatCostAndGenerateRequest {
|
||||
/// REQUIRED (even if marked optional)
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub idempotency_token: Option<String>,
|
||||
|
||||
/// REQUIRED (even if marked optional)
|
||||
/// Which model to use.
|
||||
pub model: Option<CommonSplatModel>,
|
||||
|
||||
/// The text prompt for the splat generation.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Reference images (optional).
|
||||
/// NOTE: Spherical/panorama coordinates are not yet supported per-image;
|
||||
/// multiple images are treated as multi-view input of the same world.
|
||||
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Reference video (optional).
|
||||
pub reference_video_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Whether the single reference image is a 360-degree panorama.
|
||||
pub is_panoramic: Option<bool>,
|
||||
|
||||
/// Whether to disable prompt recaptioning.
|
||||
pub disable_recaption: Option<bool>,
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod omni_gen_audio_cost_response;
|
||||
pub mod omni_gen_image_cost_response;
|
||||
pub mod omni_gen_mesh_cost_response;
|
||||
pub mod omni_gen_splat_cost_response;
|
||||
pub mod omni_gen_video_cost_response;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response body for the omni-gen mesh cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniGenMeshCostResponse {
|
||||
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,
|
||||
|
||||
/// Whether failures are refunded.
|
||||
/// True: 100% yes
|
||||
/// False: 100% no
|
||||
/// None: Unknown or variable
|
||||
pub failures_are_refunded: Option<bool>,
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response body for the omni-gen splat cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniGenSplatCostResponse {
|
||||
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,
|
||||
|
||||
/// Whether failures are refunded.
|
||||
/// True: 100% yes
|
||||
/// False: 100% no
|
||||
/// None: Unknown or variable
|
||||
pub failures_are_refunded: Option<bool>,
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod omni_gen_audio_generate_response;
|
||||
pub mod omni_gen_image_generate_response;
|
||||
pub mod omni_gen_mesh_generate_response;
|
||||
pub mod omni_gen_splat_generate_response;
|
||||
pub mod omni_gen_video_generate_response;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response body for the omni-gen mesh generation endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniGenMeshGenerateResponse {
|
||||
pub success: bool,
|
||||
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
|
||||
/// All job tokens created by this request (including the primary).
|
||||
/// For single-job requests this will contain just one element matching
|
||||
/// `inference_job_token`. For batch requests this will contain all of them.
|
||||
pub all_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response body for the omni-gen splat generation endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniGenSplatGenerateResponse {
|
||||
pub success: bool,
|
||||
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
|
||||
/// All job tokens created by this request (including the primary).
|
||||
/// For single-job requests this will contain just one element matching
|
||||
/// `inference_job_token`. For batch requests this will contain all of them.
|
||||
pub all_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod omni_gen_audio_models;
|
||||
pub mod omni_gen_image_models;
|
||||
pub mod omni_gen_mesh_models;
|
||||
pub mod omni_gen_splat_models;
|
||||
pub mod omni_gen_video_models;
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel;
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
use enums::common::generation::common_polygon_type::CommonPolygonType;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use serde_derive::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Mesh model to default to if none is specified
|
||||
const DEFAULT_MESH_MODEL : CommonMeshModel = CommonMeshModel::Hunyuan3d3;
|
||||
|
||||
/// Response body for the mesh models endpoint.
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenMeshModelsResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// A list of all models: details, features, and capabilities
|
||||
pub models: Vec<OmniGenMeshModelDetails>,
|
||||
|
||||
/// Provider-by-provider model offering and capability list,
|
||||
/// with possible capability overrides (future)
|
||||
pub providers: Vec<OmniGenMeshModelProviderDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenMeshModelProviderDetails {
|
||||
pub provider: GenerationProvider,
|
||||
pub models: Vec<OmniGenMeshProviderModelDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenMeshProviderModelDetails {
|
||||
pub model: CommonMeshModel,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub overrides: Option<OmniGenMeshModelDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenMeshModelDetails {
|
||||
|
||||
pub model: CommonMeshModel,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model_creator: Option<ModelCreator>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub full_name: Option<String>,
|
||||
|
||||
/// Additional details about the model.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_info: Option<String>,
|
||||
|
||||
/// Additional details about the model. (Brief; only a few words.)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_info_short: Option<String>,
|
||||
|
||||
/// Whether text-to-3D prompting is supported.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text_prompt_supported: Option<bool>,
|
||||
|
||||
/// Whether image-to-3D input is supported.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_input_supported: Option<bool>,
|
||||
|
||||
/// Whether sketch-to-3D input is supported.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sketch_input_supported: Option<bool>,
|
||||
|
||||
/// Whether multi-view (front/back/left/right) image input is supported.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub multi_view_supported: Option<bool>,
|
||||
|
||||
/// The mesh output types the model supports.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mesh_output_types: Option<Vec<CommonMeshOutputType>>,
|
||||
|
||||
/// The polygon types the model supports.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub polygon_types: Option<Vec<CommonPolygonType>>,
|
||||
|
||||
/// Whether a target face count can be specified.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub face_count_supported: Option<bool>,
|
||||
|
||||
/// Whether PBR (physically based rendering) material generation is supported.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pbr_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_disabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for OmniGenMeshModelDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: DEFAULT_MESH_MODEL,
|
||||
model_creator: None,
|
||||
full_name: None,
|
||||
extra_info: None,
|
||||
extra_info_short: None,
|
||||
text_prompt_supported: None,
|
||||
image_input_supported: None,
|
||||
sketch_input_supported: None,
|
||||
multi_view_supported: None,
|
||||
mesh_output_types: None,
|
||||
polygon_types: None,
|
||||
face_count_supported: None,
|
||||
pbr_supported: None,
|
||||
is_disabled: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use serde_derive::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Splat model to default to if none is specified
|
||||
const DEFAULT_SPLAT_MODEL : CommonSplatModel = CommonSplatModel::Marble1p1;
|
||||
|
||||
/// Response body for the splat models endpoint.
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenSplatModelsResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// A list of all models: details, features, and capabilities
|
||||
pub models: Vec<OmniGenSplatModelDetails>,
|
||||
|
||||
/// Provider-by-provider model offering and capability list,
|
||||
/// with possible capability overrides (future)
|
||||
pub providers: Vec<OmniGenSplatModelProviderDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenSplatModelProviderDetails {
|
||||
pub provider: GenerationProvider,
|
||||
pub models: Vec<OmniGenSplatProviderModelDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenSplatProviderModelDetails {
|
||||
pub model: CommonSplatModel,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub overrides: Option<OmniGenSplatModelDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenSplatModelDetails {
|
||||
|
||||
pub model: CommonSplatModel,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model_creator: Option<ModelCreator>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub full_name: Option<String>,
|
||||
|
||||
/// Additional details about the model.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_info: Option<String>,
|
||||
|
||||
/// Additional details about the model. (Brief; only a few words.)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_info_short: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text_prompt_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_references_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_references_max: Option<u16>,
|
||||
|
||||
/// Whether a reference video input is supported.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub video_reference_supported: Option<bool>,
|
||||
|
||||
/// Whether a 360-degree panorama reference image is supported.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub panorama_supported: Option<bool>,
|
||||
|
||||
/// Whether the "disable recaption" toggle is supported.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub disable_recaption_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_disabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for OmniGenSplatModelDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: DEFAULT_SPLAT_MODEL,
|
||||
model_creator: None,
|
||||
full_name: None,
|
||||
extra_info: None,
|
||||
extra_info_short: None,
|
||||
text_prompt_supported: None,
|
||||
image_references_supported: None,
|
||||
image_references_max: None,
|
||||
video_reference_supported: None,
|
||||
panorama_supported: None,
|
||||
disable_recaption_supported: None,
|
||||
is_disabled: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod omni_gen_mesh;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
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::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_mesh_generate_response::OmniGenMeshGenerateResponse;
|
||||
|
||||
pub const OMNI_GEN_MESH_GENERATE_PATH: &str = "/v1/omni_gen/generate/mesh";
|
||||
|
||||
pub async fn omni_gen_mesh_generate(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: OmniGenMeshCostAndGenerateRequest,
|
||||
) -> Result<OmniGenMeshGenerateResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
OMNI_GEN_MESH_GENERATE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod audio;
|
||||
pub mod image;
|
||||
pub mod mesh;
|
||||
pub mod splat;
|
||||
pub mod video;
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod omni_gen_splat;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
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::omni_gen::cost_and_generate_requests::omni_gen_splat_cost_and_generate_request::OmniGenSplatCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_splat_generate_response::OmniGenSplatGenerateResponse;
|
||||
|
||||
pub const OMNI_GEN_SPLAT_GENERATE_PATH: &str = "/v1/omni_gen/generate/splat";
|
||||
|
||||
pub async fn omni_gen_splat_generate(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: OmniGenSplatCostAndGenerateRequest,
|
||||
) -> Result<OmniGenSplatGenerateResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
OMNI_GEN_SPLAT_GENERATE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -6,9 +6,11 @@ pub mod router_aspect_ratio;
|
||||
pub mod router_audio_model;
|
||||
pub mod router_bitrate;
|
||||
pub mod router_image_model;
|
||||
pub mod router_mesh_model;
|
||||
pub mod router_provider;
|
||||
pub mod router_quality;
|
||||
pub mod router_resolution;
|
||||
pub mod router_splat_model;
|
||||
pub mod router_video_model;
|
||||
pub mod video_list_ref;
|
||||
pub mod video_ref;
|
||||
@@ -0,0 +1,62 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Common mesh (3D object) 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 RouterMeshModel {
|
||||
#[serde(rename = "hunyuan_3d_2p0")]
|
||||
Hunyuan3d2p0,
|
||||
|
||||
#[serde(rename = "hunyuan_3d_2p1")]
|
||||
Hunyuan3d2p1,
|
||||
|
||||
#[serde(rename = "hunyuan_3d_3")]
|
||||
Hunyuan3d3,
|
||||
|
||||
/// Hunyuan 3D v3 in sketch-to-3D mode. Same underlying model as
|
||||
/// [`Self::Hunyuan3d3`], but takes a sketch image plus a prompt as input.
|
||||
#[serde(rename = "hunyuan_3d_3_sketch")]
|
||||
Hunyuan3d3Sketch,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// NB: These strings must match `CommonMeshModel` — the two enums convert
|
||||
// via serde string round-trip.
|
||||
#[test]
|
||||
fn all_variants_serialize_to_common_mesh_model_strings() {
|
||||
assert_serde_round_trip(RouterMeshModel::Hunyuan3d2p0, "hunyuan_3d_2p0");
|
||||
assert_serde_round_trip(RouterMeshModel::Hunyuan3d2p1, "hunyuan_3d_2p1");
|
||||
assert_serde_round_trip(RouterMeshModel::Hunyuan3d3, "hunyuan_3d_3");
|
||||
assert_serde_round_trip(RouterMeshModel::Hunyuan3d3Sketch, "hunyuan_3d_3_sketch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_through_common_mesh_model() {
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel;
|
||||
|
||||
let cases = [
|
||||
(RouterMeshModel::Hunyuan3d2p0, CommonMeshModel::Hunyuan3d2p0),
|
||||
(RouterMeshModel::Hunyuan3d2p1, CommonMeshModel::Hunyuan3d2p1),
|
||||
(RouterMeshModel::Hunyuan3d3, CommonMeshModel::Hunyuan3d3),
|
||||
(RouterMeshModel::Hunyuan3d3Sketch, CommonMeshModel::Hunyuan3d3Sketch),
|
||||
];
|
||||
for (router_model, expected_common) in cases {
|
||||
let json = serde_json::to_string(&router_model).unwrap();
|
||||
let common: CommonMeshModel = serde_json::from_str(&json)
|
||||
.unwrap_or_else(|e| panic!("CommonMeshModel failed to parse {json}: {e}"));
|
||||
assert_eq!(common, expected_common, "for {router_model:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_serde_round_trip(model: RouterMeshModel, expected: &str) {
|
||||
let json = serde_json::to_string(&model).unwrap();
|
||||
assert_eq!(json, format!("\"{expected}\""));
|
||||
let parsed: RouterMeshModel = serde_json::from_str(&json).unwrap();
|
||||
// RouterMeshModel isn't PartialEq, so round-trip back to the wire form.
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), json);
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,5 @@ pub enum RouterProvider {
|
||||
GmiCloud,
|
||||
GrokApi,
|
||||
Seedance2Pro,
|
||||
WorldLabs,
|
||||
}
|
||||
@@ -5,9 +5,68 @@ use serde_derive::{Deserialize, Serialize};
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RouterSplatModel {
|
||||
/// Deprecated upstream; treated as [`Self::Marble1p0Draft`].
|
||||
#[serde(rename = "marble_0p1_mini")]
|
||||
Marble0p1Mini,
|
||||
|
||||
/// Deprecated upstream; treated as [`Self::Marble1p0`].
|
||||
#[serde(rename = "marble_0p1_plus")]
|
||||
Marble0p1Plus,
|
||||
|
||||
#[serde(rename = "marble_1p0")]
|
||||
Marble1p0,
|
||||
|
||||
#[serde(rename = "marble_1p0_draft")]
|
||||
Marble1p0Draft,
|
||||
|
||||
#[serde(rename = "marble_1p1")]
|
||||
Marble1p1,
|
||||
|
||||
#[serde(rename = "marble_1p1_plus")]
|
||||
Marble1p1Plus,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// NB: These strings must match `CommonSplatModel` — the two enums convert
|
||||
// via serde string round-trip.
|
||||
#[test]
|
||||
fn all_variants_serialize_to_common_splat_model_strings() {
|
||||
assert_serde_round_trip(RouterSplatModel::Marble0p1Mini, "marble_0p1_mini");
|
||||
assert_serde_round_trip(RouterSplatModel::Marble0p1Plus, "marble_0p1_plus");
|
||||
assert_serde_round_trip(RouterSplatModel::Marble1p0, "marble_1p0");
|
||||
assert_serde_round_trip(RouterSplatModel::Marble1p0Draft, "marble_1p0_draft");
|
||||
assert_serde_round_trip(RouterSplatModel::Marble1p1, "marble_1p1");
|
||||
assert_serde_round_trip(RouterSplatModel::Marble1p1Plus, "marble_1p1_plus");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_through_common_splat_model() {
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel;
|
||||
|
||||
let cases = [
|
||||
(RouterSplatModel::Marble0p1Mini, CommonSplatModel::Marble0p1Mini),
|
||||
(RouterSplatModel::Marble0p1Plus, CommonSplatModel::Marble0p1Plus),
|
||||
(RouterSplatModel::Marble1p0, CommonSplatModel::Marble1p0),
|
||||
(RouterSplatModel::Marble1p0Draft, CommonSplatModel::Marble1p0Draft),
|
||||
(RouterSplatModel::Marble1p1, CommonSplatModel::Marble1p1),
|
||||
(RouterSplatModel::Marble1p1Plus, CommonSplatModel::Marble1p1Plus),
|
||||
];
|
||||
for (router_model, expected_common) in cases {
|
||||
let json = serde_json::to_string(&router_model).unwrap();
|
||||
let common: CommonSplatModel = serde_json::from_str(&json)
|
||||
.unwrap_or_else(|e| panic!("CommonSplatModel failed to parse {json}: {e}"));
|
||||
assert_eq!(common, expected_common, "for {router_model:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_serde_round_trip(model: RouterSplatModel, expected: &str) {
|
||||
let json = serde_json::to_string(&model).unwrap();
|
||||
assert_eq!(json, format!("\"{expected}\""));
|
||||
let parsed: RouterSplatModel = serde_json::from_str(&json).unwrap();
|
||||
// RouterSplatModel isn't PartialEq, so round-trip back to the wire form.
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum VideoRef {
|
||||
MediaFileToken(MediaFileToken),
|
||||
Url(String),
|
||||
}
|
||||
@@ -8,3 +8,4 @@ pub mod router_fal_client;
|
||||
pub mod router_gmicloud_client;
|
||||
pub mod router_grok_api_client;
|
||||
pub mod router_seedance2pro_client;
|
||||
pub mod router_worldlabs_client;
|
||||
@@ -3,6 +3,7 @@ use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
|
||||
pub struct MultiRouterClient {
|
||||
@@ -11,6 +12,7 @@ pub struct MultiRouterClient {
|
||||
pub(crate) gmicloud_client: Option<RouterGmiCloudClient>,
|
||||
pub(crate) grok_api_client: Option<RouterGrokApiClient>,
|
||||
pub(crate) seedance2pro_client: Option<RouterSeedance2ProClient>,
|
||||
pub(crate) worldlabs_client: Option<RouterWorldLabsClient>,
|
||||
}
|
||||
|
||||
impl MultiRouterClient {
|
||||
@@ -38,4 +40,9 @@ impl MultiRouterClient {
|
||||
self.seedance2pro_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::Seedance2Pro))
|
||||
}
|
||||
|
||||
pub fn get_worldlabs_client_ref(&self) -> Result<&RouterWorldLabsClient, ClientError> {
|
||||
self.worldlabs_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::WorldLabs))
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
|
||||
|
||||
pub struct MultiRouterClientBuilder {
|
||||
artcraft_client: Option<RouterArtcraftClient>,
|
||||
@@ -11,6 +12,7 @@ pub struct MultiRouterClientBuilder {
|
||||
gmicloud_client: Option<RouterGmiCloudClient>,
|
||||
grok_api_client: Option<RouterGrokApiClient>,
|
||||
seedance2pro_client: Option<RouterSeedance2ProClient>,
|
||||
worldlabs_client: Option<RouterWorldLabsClient>,
|
||||
}
|
||||
|
||||
impl MultiRouterClientBuilder {
|
||||
@@ -21,6 +23,7 @@ impl MultiRouterClientBuilder {
|
||||
gmicloud_client: None,
|
||||
grok_api_client: None,
|
||||
seedance2pro_client: None,
|
||||
worldlabs_client: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +52,11 @@ impl MultiRouterClientBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_worldlabs_client(mut self, client: RouterWorldLabsClient) -> Self {
|
||||
self.worldlabs_client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> MultiRouterClient {
|
||||
MultiRouterClient {
|
||||
artcraft_client: self.artcraft_client,
|
||||
@@ -56,6 +64,7 @@ impl MultiRouterClientBuilder {
|
||||
gmicloud_client: self.gmicloud_client,
|
||||
grok_api_client: self.grok_api_client,
|
||||
seedance2pro_client: self.seedance2pro_client,
|
||||
worldlabs_client: self.worldlabs_client,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
|
||||
pub enum RouterClient {
|
||||
@@ -13,6 +14,7 @@ pub enum RouterClient {
|
||||
GmiCloud(RouterGmiCloudClient),
|
||||
GrokApi(RouterGrokApiClient),
|
||||
Seedance2Pro(RouterSeedance2ProClient),
|
||||
WorldLabs(RouterWorldLabsClient),
|
||||
}
|
||||
|
||||
impl RouterClient {
|
||||
@@ -55,4 +57,12 @@ impl RouterClient {
|
||||
_ => Err(ClientError::ClientNotConfigured(ClientType::Seedance2Pro)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_worldlabs_client_ref(&self) -> Result<&RouterWorldLabsClient, ClientError> {
|
||||
match self {
|
||||
RouterClient::WorldLabs(client) => Ok(client),
|
||||
RouterClient::Multi(multi) => multi.get_worldlabs_client_ref(),
|
||||
_ => Err(ClientError::ClientNotConfigured(ClientType::WorldLabs)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use worldlabs_api_client::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
|
||||
/// A client for the official World Labs (Marble) API.
|
||||
pub struct RouterWorldLabsClient {
|
||||
pub(crate) creds: WorldLabsApiCreds,
|
||||
}
|
||||
|
||||
impl RouterWorldLabsClient {
|
||||
pub fn new(creds: WorldLabsApiCreds) -> Self {
|
||||
Self { creds }
|
||||
}
|
||||
|
||||
pub fn new_from_raw_key(api_key: &str) -> Self {
|
||||
Self::new(WorldLabsApiCreds::new(api_key.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub enum ClientType {
|
||||
GmiCloud,
|
||||
GrokApi,
|
||||
Seedance2Pro,
|
||||
WorldLabs,
|
||||
}
|
||||
|
||||
impl Display for ClientType {
|
||||
@@ -20,6 +21,7 @@ impl Display for ClientType {
|
||||
Self::GmiCloud => write!(f, "GmiCloud"),
|
||||
Self::GrokApi => write!(f, "GrokApi"),
|
||||
Self::Seedance2Pro => write!(f, "Seedance2Pro"),
|
||||
Self::WorldLabs => write!(f, "WorldLabs"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use gmicloud_client::error::gmicloud_error::GmiCloudError;
|
||||
use grok_api_client::error::grok_error::GrokError;
|
||||
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
|
||||
use worldlabs_api_client::error::world_labs_error::WorldLabsError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProviderError {
|
||||
@@ -13,6 +14,7 @@ pub enum ProviderError {
|
||||
GmiCloud(GmiCloudError),
|
||||
Grok(GrokError),
|
||||
Seedance2Pro(Seedance2ProError),
|
||||
WorldLabs(WorldLabsError),
|
||||
}
|
||||
|
||||
impl Error for ProviderError {}
|
||||
@@ -25,6 +27,7 @@ impl Display for ProviderError {
|
||||
Self::GmiCloud(e) => write!(f, "GmiCloud provider error: {}", e),
|
||||
Self::Grok(e) => write!(f, "Grok provider error: {}", e),
|
||||
Self::Seedance2Pro(e) => write!(f, "Seedance2Pro provider error: {}", e),
|
||||
Self::WorldLabs(e) => write!(f, "WorldLabs provider error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,3 +61,9 @@ impl From<Seedance2ProError> for ProviderError {
|
||||
Self::Seedance2Pro(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WorldLabsError> for ProviderError {
|
||||
fn from(error: WorldLabsError) -> Self {
|
||||
Self::WorldLabs(error)
|
||||
}
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
use enums::common::generation::common_polygon_type::CommonPolygonType;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3::build::build_artcraft_hunyuan3d_3;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3_sketch::build::build_artcraft_hunyuan3d_3_sketch;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p0::build::build_artcraft_hunyuan_3d_2p0;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p1::build::build_artcraft_hunyuan_3d_2p1;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::build::build_fal_hunyuan3d_3;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3_sketch::build::build_fal_hunyuan3d_3_sketch;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_2p0::build::build_fal_hunyuan_3d_2p0;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_2p1::build::build_fal_hunyuan_3d_2p1;
|
||||
|
||||
/// RouterProvider-agnostic mesh (3D object) generation request. Distilled by
|
||||
/// `build2()` into a `MeshGenerationDraftOrRequest` for the selected
|
||||
/// (provider, model) pair.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateMeshRequestBuilder {
|
||||
/// Which model to use.
|
||||
pub model: RouterMeshModel,
|
||||
|
||||
/// Which provider to use.
|
||||
pub provider: RouterProvider,
|
||||
|
||||
/// The text prompt (text-to-3D, or the sketch description for sketch-to-3D).
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Reference images (optional).
|
||||
/// The primary/front input image, or the sketch image for sketch-to-3D.
|
||||
pub reference_images: Option<ImageListRef>,
|
||||
|
||||
/// Front view image (optional; multi-view input).
|
||||
/// Can be used instead of `reference_images[0]` when the other side views
|
||||
/// are supplied.
|
||||
pub front_image: Option<ImageRef>,
|
||||
|
||||
/// Back view image (optional; multi-view input).
|
||||
pub back_image: Option<ImageRef>,
|
||||
|
||||
/// Left view image (optional; multi-view input).
|
||||
pub left_image: Option<ImageRef>,
|
||||
|
||||
/// Right view image (optional; multi-view input).
|
||||
pub right_image: Option<ImageRef>,
|
||||
|
||||
/// The mesh output type to generate (normal, low-poly, or geometry-only).
|
||||
pub mesh_output_type: Option<CommonMeshOutputType>,
|
||||
|
||||
/// The polygon type to use for the generated mesh (triangle or quad).
|
||||
pub polygon_type: Option<CommonPolygonType>,
|
||||
|
||||
/// Target face count for the generated mesh.
|
||||
pub face_count: Option<u64>,
|
||||
|
||||
/// Whether to generate PBR (physically based rendering) materials.
|
||||
pub enable_pbr: Option<bool>,
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
impl Default for GenerateMeshRequestBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Artcraft,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
prompt: None,
|
||||
reference_images: None,
|
||||
front_image: None,
|
||||
back_image: None,
|
||||
left_image: None,
|
||||
right_image: None,
|
||||
mesh_output_type: None,
|
||||
polygon_type: None,
|
||||
face_count: None,
|
||||
enable_pbr: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateMeshRequestBuilder {
|
||||
|
||||
pub fn build2(self) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
match (self.provider, self.model) {
|
||||
// Artcraft
|
||||
(RouterProvider::Artcraft, RouterMeshModel::Hunyuan3d2p0) => build_artcraft_hunyuan_3d_2p0(self),
|
||||
(RouterProvider::Artcraft, RouterMeshModel::Hunyuan3d2p1) => build_artcraft_hunyuan_3d_2p1(self),
|
||||
(RouterProvider::Artcraft, RouterMeshModel::Hunyuan3d3) => build_artcraft_hunyuan3d_3(self),
|
||||
(RouterProvider::Artcraft, RouterMeshModel::Hunyuan3d3Sketch) => build_artcraft_hunyuan3d_3_sketch(self),
|
||||
// Fal
|
||||
(RouterProvider::Fal, RouterMeshModel::Hunyuan3d2p0) => build_fal_hunyuan_3d_2p0(self),
|
||||
(RouterProvider::Fal, RouterMeshModel::Hunyuan3d2p1) => build_fal_hunyuan_3d_2p1(self),
|
||||
(RouterProvider::Fal, RouterMeshModel::Hunyuan3d3) => build_fal_hunyuan3d_3(self),
|
||||
(RouterProvider::Fal, RouterMeshModel::Hunyuan3d3Sketch) => build_fal_hunyuan3d_3_sketch(self),
|
||||
_ => self.unsupported_provider_and_model(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_generate_idempotency_token(&self) -> String {
|
||||
self.idempotency_token.clone()
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
|
||||
}
|
||||
|
||||
fn unsupported_provider_and_model(&self) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(
|
||||
format!("Mesh generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
const IMAGE_URL: &str = "https://example.com/front.png";
|
||||
|
||||
mod dispatch_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn artcraft_models_dispatch() {
|
||||
let cases = [
|
||||
RouterMeshModel::Hunyuan3d2p0,
|
||||
RouterMeshModel::Hunyuan3d2p1,
|
||||
RouterMeshModel::Hunyuan3d3,
|
||||
RouterMeshModel::Hunyuan3d3Sketch,
|
||||
];
|
||||
for model in cases {
|
||||
let result = builder_with_token_image(RouterProvider::Artcraft, model).build2()
|
||||
.unwrap_or_else(|e| panic!("build should succeed for {model:?}: {e}"));
|
||||
let request = expect_request(result);
|
||||
let matches = matches!(
|
||||
(model, &request),
|
||||
(RouterMeshModel::Hunyuan3d2p0, MeshGenerationRequest::ArtcraftHunyuan3d2p0(_))
|
||||
| (RouterMeshModel::Hunyuan3d2p1, MeshGenerationRequest::ArtcraftHunyuan3d2p1(_))
|
||||
| (RouterMeshModel::Hunyuan3d3, MeshGenerationRequest::ArtcraftHunyuan3d3(_))
|
||||
| (RouterMeshModel::Hunyuan3d3Sketch, MeshGenerationRequest::ArtcraftHunyuan3d3Sketch(_))
|
||||
);
|
||||
assert!(matches, "unexpected dispatch for {model:?}: {request:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fal_hunyuan3d_3_with_image_dispatches_to_image_endpoint() {
|
||||
let result = builder_with_url_image(RouterProvider::Fal, RouterMeshModel::Hunyuan3d3)
|
||||
.build2().expect("build");
|
||||
assert!(matches!(
|
||||
expect_request(result),
|
||||
MeshGenerationRequest::FalHunyuan3d3Image(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fal_hunyuan3d_3_with_prompt_only_dispatches_to_text_endpoint() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
provider: RouterProvider::Fal,
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let result = builder.build2().expect("build");
|
||||
assert!(matches!(
|
||||
expect_request(result),
|
||||
MeshGenerationRequest::FalHunyuan3d3Text(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fal_hunyuan3d_3_sketch_dispatches() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
..builder_with_url_image(RouterProvider::Fal, RouterMeshModel::Hunyuan3d3Sketch)
|
||||
};
|
||||
let result = builder.build2().expect("build");
|
||||
assert!(matches!(
|
||||
expect_request(result),
|
||||
MeshGenerationRequest::FalHunyuan3d3Sketch(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fal_hunyuan_3d_2p0_dispatches() {
|
||||
let result = builder_with_url_image(RouterProvider::Fal, RouterMeshModel::Hunyuan3d2p0)
|
||||
.build2().expect("build");
|
||||
assert!(matches!(
|
||||
expect_request(result),
|
||||
MeshGenerationRequest::FalHunyuan3d2p0(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fal_hunyuan_3d_2p1_dispatches() {
|
||||
let result = builder_with_url_image(RouterProvider::Fal, RouterMeshModel::Hunyuan3d2p1)
|
||||
.build2().expect("build");
|
||||
assert!(matches!(
|
||||
expect_request(result),
|
||||
MeshGenerationRequest::FalHunyuan3d2p1(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
mod unsupported_combo_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn non_mesh_providers_are_unsupported() {
|
||||
for provider in [
|
||||
RouterProvider::GmiCloud,
|
||||
RouterProvider::GrokApi,
|
||||
RouterProvider::Seedance2Pro,
|
||||
RouterProvider::WorldLabs,
|
||||
] {
|
||||
let result = builder_with_url_image(provider, RouterMeshModel::Hunyuan3d3).build2();
|
||||
assert!(
|
||||
matches!(result, Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(_))),
|
||||
"expected unsupported error for {provider:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn builder_with_url_image(provider: RouterProvider, model: RouterMeshModel) -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
provider,
|
||||
model,
|
||||
reference_images: Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn builder_with_token_image(provider: RouterProvider, model: RouterMeshModel) -> GenerateMeshRequestBuilder {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
GenerateMeshRequestBuilder {
|
||||
provider,
|
||||
model,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_test123".to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_request(result: MeshGenerationDraftOrRequest) -> MeshGenerationRequest {
|
||||
match result {
|
||||
MeshGenerationDraftOrRequest::Request(request) => request,
|
||||
MeshGenerationDraftOrRequest::Draft(draft) => panic!("expected Request, got Draft: {draft:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftMeshResponsePayload {
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
pub all_inference_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalMeshResponsePayload {
|
||||
pub request_id: Option<String>,
|
||||
pub gateway_request_id: Option<String>,
|
||||
|
||||
/// Queue-mode status URL (fal's queue/polling flow). `None` for webhook
|
||||
/// dispatch — the webhook callback drives status updates instead.
|
||||
pub maybe_status_url: Option<String>,
|
||||
|
||||
/// Queue-mode response URL (fal's queue/polling flow). `None` for webhook
|
||||
/// dispatch.
|
||||
pub maybe_response_url: Option<String>,
|
||||
|
||||
/// The outbound request that was sent to Fal.
|
||||
/// Stored as a trait object so any Request type can be captured.
|
||||
/// Use `format!("{:?}", ...)` or `format!("{:#?}", ...)` to print.
|
||||
pub maybe_outbound_request: Option<Arc<dyn Debug + Send + Sync>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GenerateMeshResponse {
|
||||
Artcraft(ArtcraftMeshResponsePayload),
|
||||
Fal(FalMeshResponsePayload),
|
||||
}
|
||||
|
||||
impl GenerateMeshResponse {
|
||||
pub fn get_artcraft_payload(&self) -> Option<ArtcraftMeshResponsePayload> {
|
||||
match self {
|
||||
Self::Artcraft(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fal_payload(&self) -> Option<FalMeshResponsePayload> {
|
||||
match self {
|
||||
Self::Fal(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/// The result of estimating the cost of a mesh generation plan.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MeshGenerationCostEstimate {
|
||||
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,
|
||||
|
||||
/// Whether failures are refunded.
|
||||
/// True: 100% yes
|
||||
/// False: 100% no
|
||||
/// None: Unknown or variable
|
||||
pub failures_are_refunded: Option<bool>,
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_context::MeshGenerationDraftContext;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
|
||||
/**
|
||||
* Wrapper for all mesh generation draft requests.
|
||||
*
|
||||
* No mesh provider currently needs a draft phase: fal takes media URLs
|
||||
* directly and Artcraft takes media tokens, so `build2()` always returns a
|
||||
* `Request`. The enum exists so the mesh pipeline matches the audio/image
|
||||
* draft-or-request shape and providers that need pre-upload can slot in
|
||||
* later.
|
||||
*/
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MeshGenerationDraftRequest {}
|
||||
|
||||
impl MeshGenerationDraftRequest {
|
||||
|
||||
pub fn get_provider(&self) -> RouterProvider {
|
||||
match *self {}
|
||||
}
|
||||
|
||||
/// Return a cost estimate to fulfill the request.
|
||||
pub fn estimate_cost(&self) -> Result<MeshGenerationCostEstimate, ArtcraftRouterError> {
|
||||
match *self {}
|
||||
}
|
||||
|
||||
/// Finalize the draft request before generation
|
||||
/// This may involve uploading media to the provider.
|
||||
pub async fn finalize(self, _draft_context: MeshGenerationDraftContext<'_>) -> Result<MeshGenerationRequest, ArtcraftRouterError> {
|
||||
match self {}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::client::router_client::RouterClient;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MeshGenerationDraftContext<'a> {
|
||||
/// Optional: the router client, needed for providers that require authentication.
|
||||
pub client: Option<&'a RouterClient>,
|
||||
|
||||
/// Optional context: a map of Media File Tokens to their ArtCraft URLs
|
||||
/// Only needed if we have to fetch these assets and upload them to another provider.
|
||||
pub media_file_to_artcraft_url_map: Option<&'a HashMap<MediaFileToken, String>>,
|
||||
}
|
||||
|
||||
impl Debug for MeshGenerationDraftContext<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("MeshGenerationDraftContext")
|
||||
.field("client", &self.client.is_some())
|
||||
.field("media_file_to_artcraft_url_map", &self.media_file_to_artcraft_url_map)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft::MeshGenerationDraftRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MeshGenerationDraftOrRequest {
|
||||
Draft(MeshGenerationDraftRequest),
|
||||
Request(MeshGenerationRequest),
|
||||
}
|
||||
|
||||
impl MeshGenerationDraftOrRequest {
|
||||
|
||||
pub fn get_provider(&self) -> RouterProvider {
|
||||
match self {
|
||||
Self::Draft(draft) => draft.get_provider(),
|
||||
Self::Request(request) => request.get_provider(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> Result<MeshGenerationCostEstimate, ArtcraftRouterError> {
|
||||
match self {
|
||||
MeshGenerationDraftOrRequest::Draft(draft) => draft.estimate_cost(),
|
||||
MeshGenerationDraftOrRequest::Request(request) => request.estimate_cost(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3::cost::ArtcraftHunyuan3d3CostState;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3::request::ArtcraftHunyuan3d3RequestState;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3_sketch::cost::ArtcraftHunyuan3d3SketchCostState;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3_sketch::request::ArtcraftHunyuan3d3SketchRequestState;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p0::cost::ArtcraftHunyuan3d2p0CostState;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p0::request::ArtcraftHunyuan3d2p0RequestState;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p1::cost::ArtcraftHunyuan3d2p1CostState;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p1::request::ArtcraftHunyuan3d2p1RequestState;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::cost::{
|
||||
FalHunyuan3d3ImageCostState, FalHunyuan3d3TextCostState,
|
||||
};
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::request::{
|
||||
FalHunyuan3d3ImageRequestState, FalHunyuan3d3TextRequestState,
|
||||
};
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3_sketch::cost::FalHunyuan3d3SketchCostState;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3_sketch::request::FalHunyuan3d3SketchRequestState;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_2p0::cost::FalHunyuan3d2p0CostState;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_2p0::request::FalHunyuan3d2p0RequestState;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_2p1::cost::FalHunyuan3d2p1CostState;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_2p1::request::FalHunyuan3d2p1RequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MeshGenerationRequest {
|
||||
ArtcraftHunyuan3d2p0(ArtcraftHunyuan3d2p0RequestState),
|
||||
ArtcraftHunyuan3d2p1(ArtcraftHunyuan3d2p1RequestState),
|
||||
ArtcraftHunyuan3d3(ArtcraftHunyuan3d3RequestState),
|
||||
ArtcraftHunyuan3d3Sketch(ArtcraftHunyuan3d3SketchRequestState),
|
||||
FalHunyuan3d2p0(FalHunyuan3d2p0RequestState),
|
||||
FalHunyuan3d2p1(FalHunyuan3d2p1RequestState),
|
||||
FalHunyuan3d3Image(FalHunyuan3d3ImageRequestState),
|
||||
FalHunyuan3d3Text(FalHunyuan3d3TextRequestState),
|
||||
FalHunyuan3d3Sketch(FalHunyuan3d3SketchRequestState),
|
||||
}
|
||||
|
||||
impl MeshGenerationRequest {
|
||||
|
||||
pub fn get_provider(&self) -> RouterProvider {
|
||||
match self {
|
||||
Self::ArtcraftHunyuan3d2p0(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftHunyuan3d2p1(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftHunyuan3d3(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftHunyuan3d3Sketch(_) => RouterProvider::Artcraft,
|
||||
Self::FalHunyuan3d2p0(_) => RouterProvider::Fal,
|
||||
Self::FalHunyuan3d2p1(_) => RouterProvider::Fal,
|
||||
Self::FalHunyuan3d3Image(_) => RouterProvider::Fal,
|
||||
Self::FalHunyuan3d3Text(_) => RouterProvider::Fal,
|
||||
Self::FalHunyuan3d3Sketch(_) => RouterProvider::Fal,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a cost estimate to fulfill the request.
|
||||
pub fn estimate_cost(&self) -> Result<MeshGenerationCostEstimate, ArtcraftRouterError> {
|
||||
match self {
|
||||
MeshGenerationRequest::ArtcraftHunyuan3d2p0(request) => Ok(ArtcraftHunyuan3d2p0CostState::from_request(request).estimate_cost()),
|
||||
MeshGenerationRequest::ArtcraftHunyuan3d2p1(request) => Ok(ArtcraftHunyuan3d2p1CostState::from_request(request).estimate_cost()),
|
||||
MeshGenerationRequest::ArtcraftHunyuan3d3(request) => Ok(ArtcraftHunyuan3d3CostState::from_request(request).estimate_cost()),
|
||||
MeshGenerationRequest::ArtcraftHunyuan3d3Sketch(request) => Ok(ArtcraftHunyuan3d3SketchCostState::from_request(request).estimate_cost()),
|
||||
MeshGenerationRequest::FalHunyuan3d2p0(request) => Ok(FalHunyuan3d2p0CostState::from_request(request).estimate_cost()),
|
||||
MeshGenerationRequest::FalHunyuan3d2p1(request) => Ok(FalHunyuan3d2p1CostState::from_request(request).estimate_cost()),
|
||||
MeshGenerationRequest::FalHunyuan3d3Image(request) => Ok(FalHunyuan3d3ImageCostState::from_request(request).estimate_cost()),
|
||||
MeshGenerationRequest::FalHunyuan3d3Text(request) => Ok(FalHunyuan3d3TextCostState::from_request(request).estimate_cost()),
|
||||
MeshGenerationRequest::FalHunyuan3d3Sketch(request) => Ok(FalHunyuan3d3SketchCostState::from_request(request).estimate_cost()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send the mesh generation request
|
||||
/// If successful, returns the job IDs.
|
||||
pub async fn send_request(&self, client: &RouterClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
MeshGenerationRequest::ArtcraftHunyuan3d2p0(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
MeshGenerationRequest::ArtcraftHunyuan3d2p1(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
MeshGenerationRequest::ArtcraftHunyuan3d3(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
MeshGenerationRequest::ArtcraftHunyuan3d3Sketch(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
MeshGenerationRequest::FalHunyuan3d2p0(request) => {
|
||||
let client_ref = client.get_fal_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
MeshGenerationRequest::FalHunyuan3d2p1(request) => {
|
||||
let client_ref = client.get_fal_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
MeshGenerationRequest::FalHunyuan3d3Image(request) => {
|
||||
let client_ref = client.get_fal_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
MeshGenerationRequest::FalHunyuan3d3Text(request) => {
|
||||
let client_ref = client.get_fal_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
MeshGenerationRequest::FalHunyuan3d3Sketch(request) => {
|
||||
let client_ref = client.get_fal_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod generate_mesh_request_builder;
|
||||
pub mod generate_mesh_response;
|
||||
pub mod mesh_generation_cost_estimate;
|
||||
pub mod mesh_generation_draft;
|
||||
pub mod mesh_generation_draft_context;
|
||||
pub mod mesh_generation_draft_or_request;
|
||||
pub mod mesh_generation_request;
|
||||
pub mod providers;
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::providers::artcraft::resolve::{
|
||||
resolve_image_list_ref, resolve_image_ref,
|
||||
};
|
||||
|
||||
/// Build an `OmniGenMeshCostAndGenerateRequest` from the builder. The
|
||||
/// Artcraft provider is a thin forwarding shim: the storyteller-web omni
|
||||
/// mesh endpoint owns per-model validation and option mapping, so the
|
||||
/// builder's options are passed through as-is.
|
||||
pub fn build_artcraft_omni_mesh_request(
|
||||
mut builder: GenerateMeshRequestBuilder,
|
||||
model: CommonMeshModelEnum,
|
||||
) -> Result<OmniGenMeshCostAndGenerateRequest, ArtcraftRouterError> {
|
||||
let reference_image_media_tokens = resolve_image_list_ref(builder.reference_images.take())?;
|
||||
let front_image_media_token = resolve_image_ref(builder.front_image.take())?;
|
||||
let back_image_media_token = resolve_image_ref(builder.back_image.take())?;
|
||||
let left_image_media_token = resolve_image_ref(builder.left_image.take())?;
|
||||
let right_image_media_token = resolve_image_ref(builder.right_image.take())?;
|
||||
let idempotency_token = builder.get_or_generate_idempotency_token();
|
||||
|
||||
Ok(OmniGenMeshCostAndGenerateRequest {
|
||||
idempotency_token: Some(idempotency_token),
|
||||
model: Some(model),
|
||||
prompt: builder.prompt.take(),
|
||||
reference_image_media_tokens,
|
||||
front_image_media_token,
|
||||
back_image_media_token,
|
||||
left_image_media_token,
|
||||
right_image_media_token,
|
||||
mesh_output_type: builder.mesh_output_type,
|
||||
polygon_type: builder.polygon_type,
|
||||
face_count: builder.face_count,
|
||||
enable_pbr: builder.enable_pbr,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
use enums::common::generation::common_polygon_type::CommonPolygonType;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_fields_are_forwarded() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_ref".to_string()),
|
||||
])),
|
||||
front_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_front".to_string()))),
|
||||
back_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_back".to_string()))),
|
||||
left_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_left".to_string()))),
|
||||
right_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_right".to_string()))),
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
polygon_type: Some(CommonPolygonType::Quad),
|
||||
face_count: Some(100_000),
|
||||
enable_pbr: Some(true),
|
||||
idempotency_token: Some("test-token".to_string()),
|
||||
..base_builder()
|
||||
};
|
||||
|
||||
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d3)
|
||||
.expect("build");
|
||||
|
||||
assert_eq!(request.idempotency_token.as_deref(), Some("test-token"));
|
||||
assert_eq!(request.model, Some(CommonMeshModelEnum::Hunyuan3d3));
|
||||
assert_eq!(request.prompt.as_deref(), Some("a red ceramic teapot"));
|
||||
assert_eq!(request.reference_image_media_tokens.as_ref().map(|t| t.len()), Some(1));
|
||||
assert_eq!(request.front_image_media_token.as_ref().map(|t| t.as_str()), Some("mf_front"));
|
||||
assert_eq!(request.back_image_media_token.as_ref().map(|t| t.as_str()), Some("mf_back"));
|
||||
assert_eq!(request.left_image_media_token.as_ref().map(|t| t.as_str()), Some("mf_left"));
|
||||
assert_eq!(request.right_image_media_token.as_ref().map(|t| t.as_str()), Some("mf_right"));
|
||||
assert_eq!(request.mesh_output_type, Some(CommonMeshOutputType::LowPoly));
|
||||
assert_eq!(request.polygon_type, Some(CommonPolygonType::Quad));
|
||||
assert_eq!(request.face_count, Some(100_000));
|
||||
assert_eq!(request.enable_pbr, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idempotency_token_is_generated_when_missing() {
|
||||
let request = build_artcraft_omni_mesh_request(base_builder(), CommonMeshModelEnum::Hunyuan3d3)
|
||||
.expect("build");
|
||||
assert!(request.idempotency_token.is_some());
|
||||
assert!(!request.idempotency_token.unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_url_references_are_rejected() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec!["https://example.com/a.png".to_string()])),
|
||||
..base_builder()
|
||||
};
|
||||
let result = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d3);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn side_image_url_references_are_rejected() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::Url("https://example.com/back.png".to_string())),
|
||||
..base_builder()
|
||||
};
|
||||
let result = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d3);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3::request::ArtcraftHunyuan3d3RequestState;
|
||||
|
||||
pub fn build_artcraft_hunyuan3d_3(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d3)?;
|
||||
let state = ArtcraftHunyuan3d3RequestState { request };
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftHunyuan3d3(state)))
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3::request::ArtcraftHunyuan3d3RequestState;
|
||||
|
||||
const NORMAL_COST_IN_USD_CENTS: u64 = 49;
|
||||
const LOW_POLY_COST_IN_USD_CENTS: u64 = 59;
|
||||
const GEOMETRY_COST_IN_USD_CENTS: u64 = 30;
|
||||
const ADD_ON_COST_IN_USD_CENTS: u64 = 20;
|
||||
|
||||
/// Hunyuan 3D v3 via Artcraft: flat base price by output type, plus a flat
|
||||
/// add-on for each extra option (PBR materials, custom face count, and
|
||||
/// multi-view side images).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d3CostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d3CostState {
|
||||
pub fn from_request(request: &ArtcraftHunyuan3d3RequestState) -> Self {
|
||||
let request = &request.request;
|
||||
|
||||
let mut cost = match request.mesh_output_type {
|
||||
None | Some(CommonMeshOutputType::Normal) => NORMAL_COST_IN_USD_CENTS,
|
||||
Some(CommonMeshOutputType::LowPoly) => LOW_POLY_COST_IN_USD_CENTS,
|
||||
Some(CommonMeshOutputType::Geometry) => GEOMETRY_COST_IN_USD_CENTS,
|
||||
};
|
||||
if request.enable_pbr.unwrap_or(false) {
|
||||
cost += ADD_ON_COST_IN_USD_CENTS;
|
||||
}
|
||||
if request.face_count.is_some() {
|
||||
cost += ADD_ON_COST_IN_USD_CENTS;
|
||||
}
|
||||
let uses_multi_view = request.back_image_media_token.is_some()
|
||||
|| request.left_image_media_token.is_some()
|
||||
|| request.right_image_media_token.is_some();
|
||||
if uses_multi_view {
|
||||
cost += ADD_ON_COST_IN_USD_CENTS;
|
||||
}
|
||||
|
||||
Self { cost_in_usd_cents: cost }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
MeshGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_polygon_type::CommonPolygonType;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod base_costs {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_output_type_is_forty_nine_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_is_forty_nine_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Normal),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_poly_is_fifty_nine_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 59);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_is_thirty_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_mode_prices_the_same_as_image_mode() {
|
||||
// Text-only request (no image references).
|
||||
let text_builder = GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(text_builder), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polygon_type_does_not_change_the_price() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
polygon_type: Some(CommonPolygonType::Quad),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49);
|
||||
}
|
||||
}
|
||||
|
||||
mod add_ons {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pbr_adds_twenty_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(true),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbr_false_adds_nothing() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(false),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_count_adds_twenty_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
face_count: Some(100_000),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_side_image_adds_twenty_cents_at_most_once() {
|
||||
for side in ["back", "left", "right"] {
|
||||
let mut builder = base_builder();
|
||||
let image = Some(ImageRef::MediaFileToken(MediaFileToken::new(format!("mf_{side}"))));
|
||||
match side {
|
||||
"back" => builder.back_image = image,
|
||||
"left" => builder.left_image = image,
|
||||
_ => builder.right_image = image,
|
||||
}
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20, "for {side} image");
|
||||
}
|
||||
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_back".to_string()))),
|
||||
left_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_left".to_string()))),
|
||||
right_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_right".to_string()))),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_add_ons_stack() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
enable_pbr: Some(true),
|
||||
face_count: Some(100_000),
|
||||
back_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_back".to_string()))),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 59 + 20 + 20 + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_with_all_add_ons() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
enable_pbr: Some(true),
|
||||
face_count: Some(10_000),
|
||||
left_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_left".to_string()))),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 30 + 20 + 20 + 20);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Artcraft,
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_front".to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d3RequestState {
|
||||
pub request: OmniGenMeshCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d3RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::test_helpers::get_artcraft_client;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn text_to_mesh_teapot() {
|
||||
let response = run_pipeline(GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("A red ceramic teapot with a curved spout.".to_string()),
|
||||
..Default::default()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateMeshResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
async fn run_pipeline(builder: GenerateMeshRequestBuilder) -> GenerateMeshResponse {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
MeshGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateMeshResponse::Artcraft(p) => {
|
||||
println!("inference_job_token={:?}", p.inference_job_token);
|
||||
println!("all_inference_job_tokens={:?}", p.all_inference_job_tokens);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3_sketch::request::ArtcraftHunyuan3d3SketchRequestState;
|
||||
|
||||
pub fn build_artcraft_hunyuan3d_3_sketch(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d3Sketch)?;
|
||||
let state = ArtcraftHunyuan3d3SketchRequestState { request };
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftHunyuan3d3Sketch(state)))
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3_sketch::request::ArtcraftHunyuan3d3SketchRequestState;
|
||||
|
||||
const BASE_COST_IN_USD_CENTS: u64 = 49;
|
||||
const ADD_ON_COST_IN_USD_CENTS: u64 = 20;
|
||||
|
||||
/// Hunyuan 3D v3 sketch-to-3D via Artcraft: flat base price plus a flat
|
||||
/// add-on for each extra option (PBR materials, custom face count).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d3SketchCostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d3SketchCostState {
|
||||
pub fn from_request(request: &ArtcraftHunyuan3d3SketchRequestState) -> Self {
|
||||
let request = &request.request;
|
||||
|
||||
let mut cost = BASE_COST_IN_USD_CENTS;
|
||||
if request.enable_pbr.unwrap_or(false) {
|
||||
cost += ADD_ON_COST_IN_USD_CENTS;
|
||||
}
|
||||
if request.face_count.is_some() {
|
||||
cost += ADD_ON_COST_IN_USD_CENTS;
|
||||
}
|
||||
|
||||
Self { cost_in_usd_cents: cost }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
MeshGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
#[test]
|
||||
fn base_cost_is_forty_nine_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbr_adds_twenty_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(true),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbr_false_adds_nothing() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(false),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_count_adds_twenty_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
face_count: Some(100_000),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_add_ons_stack() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(true),
|
||||
face_count: Some(100_000),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20 + 20);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3Sketch,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_sketch".to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d3SketchRequestState {
|
||||
pub request: OmniGenMeshCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d3SketchRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p0::request::ArtcraftHunyuan3d2p0RequestState;
|
||||
|
||||
pub fn build_artcraft_hunyuan_3d_2p0(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d2p0)?;
|
||||
let state = ArtcraftHunyuan3d2p0RequestState { request };
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftHunyuan3d2p0(state)))
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p0::request::ArtcraftHunyuan3d2p0RequestState;
|
||||
|
||||
const TEXTURED_COST_IN_USD_CENTS: u64 = 63;
|
||||
const GEOMETRY_COST_IN_USD_CENTS: u64 = 21;
|
||||
|
||||
/// Hunyuan 3D 2.0 via Artcraft: flat price by output type. Geometry-only
|
||||
/// (white mesh) is cheaper; every other output type prices as textured.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d2p0CostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d2p0CostState {
|
||||
pub fn from_request(request: &ArtcraftHunyuan3d2p0RequestState) -> Self {
|
||||
let cost = match request.request.mesh_output_type {
|
||||
Some(CommonMeshOutputType::Geometry) => GEOMETRY_COST_IN_USD_CENTS,
|
||||
None | Some(_) => TEXTURED_COST_IN_USD_CENTS,
|
||||
};
|
||||
Self { cost_in_usd_cents: cost }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
MeshGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_output_type_is_sixty_three_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 63);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_is_sixty_three_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Normal),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 63);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_is_twenty_one_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 21);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p0,
|
||||
provider: RouterProvider::Artcraft,
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_front".to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d2p0RequestState {
|
||||
pub request: OmniGenMeshCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d2p0RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p1::request::ArtcraftHunyuan3d2p1RequestState;
|
||||
|
||||
pub fn build_artcraft_hunyuan_3d_2p1(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d2p1)?;
|
||||
let state = ArtcraftHunyuan3d2p1RequestState { request };
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftHunyuan3d2p1(state)))
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p1::request::ArtcraftHunyuan3d2p1RequestState;
|
||||
|
||||
const TEXTURED_COST_IN_USD_CENTS: u64 = 117;
|
||||
const GEOMETRY_COST_IN_USD_CENTS: u64 = 39;
|
||||
|
||||
/// Hunyuan 3D 2.1 via Artcraft: flat price by output type. Geometry-only
|
||||
/// (white mesh) is cheaper; every other output type prices as textured.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d2p1CostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d2p1CostState {
|
||||
pub fn from_request(request: &ArtcraftHunyuan3d2p1RequestState) -> Self {
|
||||
let cost = match request.request.mesh_output_type {
|
||||
Some(CommonMeshOutputType::Geometry) => GEOMETRY_COST_IN_USD_CENTS,
|
||||
None | Some(_) => TEXTURED_COST_IN_USD_CENTS,
|
||||
};
|
||||
Self { cost_in_usd_cents: cost }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
MeshGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_output_type_is_one_seventeen_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 117);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_is_one_seventeen_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Normal),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 117);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_is_thirty_nine_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 39);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p1,
|
||||
provider: RouterProvider::Artcraft,
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_front".to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d2p1RequestState {
|
||||
pub request: OmniGenMeshCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d2p1RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
pub(crate) mod build_common;
|
||||
pub(crate) mod request_common;
|
||||
pub(crate) mod resolve;
|
||||
pub mod hunyuan3d_3;
|
||||
pub mod hunyuan3d_3_sketch;
|
||||
pub mod hunyuan_3d_2p0;
|
||||
pub mod hunyuan_3d_2p1;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::mesh::omni_gen_mesh::omni_gen_mesh_generate;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::{
|
||||
ArtcraftMeshResponsePayload, GenerateMeshResponse,
|
||||
};
|
||||
|
||||
/// Send a mesh generation request to the Artcraft omni-gen endpoint.
|
||||
/// All Artcraft model request states delegate to this function.
|
||||
pub async fn send_artcraft_omni_mesh_request(
|
||||
request: &OmniGenMeshCostAndGenerateRequest,
|
||||
client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
let response = omni_gen_mesh_generate(
|
||||
&client.api_host,
|
||||
Some(&client.credentials),
|
||||
request.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateMeshResponse::Artcraft(ArtcraftMeshResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
all_inference_job_tokens: response.all_job_tokens,
|
||||
}))
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
//! Shared resolve helpers for the Artcraft mesh provider.
|
||||
//!
|
||||
//! Artcraft only accepts media file tokens — raw URLs are rejected.
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
pub(super) fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Option<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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resolve_image_ref(
|
||||
image_ref: Option<ImageRef>,
|
||||
) -> Result<Option<MediaFileToken>, ArtcraftRouterError> {
|
||||
match image_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageRef::MediaFileToken(token)) => Ok(Some(token)),
|
||||
Some(ImageRef::Url(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
use enums::common::generation::common_polygon_type::CommonPolygonType;
|
||||
use fal_client::requests::api::mesh::image::hunyuan3d_3_image_to_mesh::api::{
|
||||
Hunyuan3d3ImageToMeshGenerateType, Hunyuan3d3ImageToMeshPolygonType, Hunyuan3d3ImageToMeshRequest,
|
||||
};
|
||||
use fal_client::requests::api::mesh::text::hunyuan3d_3_text_to_mesh::api::{
|
||||
Hunyuan3d3TextToMeshGenerateType, Hunyuan3d3TextToMeshPolygonType, Hunyuan3d3TextToMeshRequest,
|
||||
};
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::request::{
|
||||
FalHunyuan3d3ImageRequestState, FalHunyuan3d3TextRequestState,
|
||||
};
|
||||
use crate::generate::generate_mesh::providers::fal::resolve::{
|
||||
plan_face_count, plan_primary_image_url, plan_side_image_url,
|
||||
};
|
||||
use crate::generate::generate_mesh::providers::reject_unsupported::{
|
||||
reject_unsupported_image_ref, reject_unsupported_option,
|
||||
};
|
||||
|
||||
/// Hunyuan 3D v3 combines fal's image-to-3d and text-to-3d endpoints under a
|
||||
/// single router model. The request shape picks the endpoint: any image input
|
||||
/// (reference images or `front_image`) dispatches to image-to-3d; otherwise a
|
||||
/// prompt dispatches to text-to-3d.
|
||||
pub fn build_fal_hunyuan3d_3(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_fal_hunyuan3d_3_state(builder)?;
|
||||
let request = match state {
|
||||
FalHunyuan3d3State::Image(state) => MeshGenerationRequest::FalHunyuan3d3Image(state),
|
||||
FalHunyuan3d3State::Text(state) => MeshGenerationRequest::FalHunyuan3d3Text(state),
|
||||
};
|
||||
Ok(MeshGenerationDraftOrRequest::Request(request))
|
||||
}
|
||||
|
||||
/// The endpoint selected by the request shape.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum FalHunyuan3d3State {
|
||||
Image(FalHunyuan3d3ImageRequestState),
|
||||
Text(FalHunyuan3d3TextRequestState),
|
||||
}
|
||||
|
||||
pub(crate) fn build_fal_hunyuan3d_3_state(
|
||||
mut builder: GenerateMeshRequestBuilder,
|
||||
) -> Result<FalHunyuan3d3State, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
let maybe_front_url = plan_primary_image_url(
|
||||
builder.reference_images.take(),
|
||||
builder.front_image.take(),
|
||||
strategy,
|
||||
)?;
|
||||
let face_count = plan_face_count(builder.face_count, strategy)?;
|
||||
|
||||
match maybe_front_url {
|
||||
Some(image_url) => {
|
||||
// Image mode. The image endpoint has no prompt parameter.
|
||||
reject_unsupported_option("prompt", builder.prompt.as_ref(), strategy)?;
|
||||
|
||||
let request = Hunyuan3d3ImageToMeshRequest {
|
||||
image_url,
|
||||
back_image_url: plan_side_image_url(builder.back_image.take())?,
|
||||
left_image_url: plan_side_image_url(builder.left_image.take())?,
|
||||
right_image_url: plan_side_image_url(builder.right_image.take())?,
|
||||
face_count,
|
||||
generate_type: builder.mesh_output_type.map(to_image_generate_type),
|
||||
polygon_type: builder.polygon_type.map(to_image_polygon_type),
|
||||
enable_pbr: builder.enable_pbr,
|
||||
};
|
||||
Ok(FalHunyuan3d3State::Image(FalHunyuan3d3ImageRequestState { request }))
|
||||
}
|
||||
None => {
|
||||
// Text mode. Multi-view side images only make sense alongside a front
|
||||
// image, so they're unsupported here.
|
||||
reject_unsupported_image_ref("back_image", builder.back_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("left_image", builder.left_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("right_image", builder.right_image.as_ref(), strategy)?;
|
||||
|
||||
let prompt = builder.prompt.take().ok_or_else(|| {
|
||||
ArtcraftRouterError::InvalidInput(
|
||||
"Hunyuan 3D v3 requires an input image or a prompt".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let request = Hunyuan3d3TextToMeshRequest {
|
||||
prompt,
|
||||
face_count,
|
||||
generate_type: builder.mesh_output_type.map(to_text_generate_type),
|
||||
polygon_type: builder.polygon_type.map(to_text_polygon_type),
|
||||
enable_pbr: builder.enable_pbr,
|
||||
};
|
||||
Ok(FalHunyuan3d3State::Text(FalHunyuan3d3TextRequestState { request }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Enum mapping helpers ──
|
||||
|
||||
fn to_image_generate_type(output_type: CommonMeshOutputType) -> Hunyuan3d3ImageToMeshGenerateType {
|
||||
match output_type {
|
||||
CommonMeshOutputType::Normal => Hunyuan3d3ImageToMeshGenerateType::Normal,
|
||||
CommonMeshOutputType::LowPoly => Hunyuan3d3ImageToMeshGenerateType::LowPoly,
|
||||
CommonMeshOutputType::Geometry => Hunyuan3d3ImageToMeshGenerateType::Geometry,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_image_polygon_type(polygon_type: CommonPolygonType) -> Hunyuan3d3ImageToMeshPolygonType {
|
||||
match polygon_type {
|
||||
CommonPolygonType::Triangle => Hunyuan3d3ImageToMeshPolygonType::Triangle,
|
||||
CommonPolygonType::Quad => Hunyuan3d3ImageToMeshPolygonType::Quadrilateral,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_text_generate_type(output_type: CommonMeshOutputType) -> Hunyuan3d3TextToMeshGenerateType {
|
||||
match output_type {
|
||||
CommonMeshOutputType::Normal => Hunyuan3d3TextToMeshGenerateType::Normal,
|
||||
CommonMeshOutputType::LowPoly => Hunyuan3d3TextToMeshGenerateType::LowPoly,
|
||||
CommonMeshOutputType::Geometry => Hunyuan3d3TextToMeshGenerateType::Geometry,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_text_polygon_type(polygon_type: CommonPolygonType) -> Hunyuan3d3TextToMeshPolygonType {
|
||||
match polygon_type {
|
||||
CommonPolygonType::Triangle => Hunyuan3d3TextToMeshPolygonType::Triangle,
|
||||
CommonPolygonType::Quad => Hunyuan3d3TextToMeshPolygonType::Quadrilateral,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
use super::*;
|
||||
|
||||
const FRONT_URL: &str = "https://example.com/front.png";
|
||||
const BACK_URL: &str = "https://example.com/back.png";
|
||||
|
||||
mod shape_dispatch {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reference_image_dispatches_to_image_mode() {
|
||||
let state = build_fal_hunyuan3d_3_state(image_builder()).expect("build");
|
||||
let image = expect_image(state);
|
||||
assert_eq!(image.request.image_url, FRONT_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn front_image_dispatches_to_image_mode() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
reference_images: None,
|
||||
front_image: Some(ImageRef::Url(FRONT_URL.to_string())),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan3d_3_state(builder).expect("build");
|
||||
let image = expect_image(state);
|
||||
assert_eq!(image.request.image_url, FRONT_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_only_dispatches_to_text_mode() {
|
||||
let state = build_fal_hunyuan3d_3_state(text_builder()).expect("build");
|
||||
let text = expect_text(state);
|
||||
assert_eq!(text.request.prompt, "a red ceramic teapot");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neither_image_nor_prompt_is_rejected() {
|
||||
let result = build_fal_hunyuan3d_3_state(base_builder());
|
||||
assert!(matches!(result, Err(ArtcraftRouterError::InvalidInput(_))));
|
||||
}
|
||||
}
|
||||
|
||||
mod image_mode {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn side_images_map_to_multi_view_params() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::Url(BACK_URL.to_string())),
|
||||
left_image: Some(ImageRef::Url("https://example.com/left.png".to_string())),
|
||||
right_image: Some(ImageRef::Url("https://example.com/right.png".to_string())),
|
||||
..image_builder()
|
||||
};
|
||||
let image = expect_image(build_fal_hunyuan3d_3_state(builder).expect("build"));
|
||||
assert_eq!(image.request.back_image_url.as_deref(), Some(BACK_URL));
|
||||
assert_eq!(image.request.left_image_url.as_deref(), Some("https://example.com/left.png"));
|
||||
assert_eq!(image.request.right_image_url.as_deref(), Some("https://example.com/right.png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_map_through() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
polygon_type: Some(CommonPolygonType::Quad),
|
||||
face_count: Some(100_000),
|
||||
enable_pbr: Some(true),
|
||||
..image_builder()
|
||||
};
|
||||
let image = expect_image(build_fal_hunyuan3d_3_state(builder).expect("build"));
|
||||
assert!(matches!(image.request.generate_type, Some(Hunyuan3d3ImageToMeshGenerateType::LowPoly)));
|
||||
assert!(matches!(image.request.polygon_type, Some(Hunyuan3d3ImageToMeshPolygonType::Quadrilateral)));
|
||||
assert_eq!(image.request.face_count, Some(100_000));
|
||||
assert_eq!(image.request.enable_pbr, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_with_image_errors_out_under_error_out() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..image_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan3d_3_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_with_image_is_dropped_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
..image_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan3d_3_state(builder).expect("build"),
|
||||
FalHunyuan3d3State::Image(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_tokens_are_rejected() {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_test123".to_string()),
|
||||
])),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan3d_3_state(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
mod text_mode {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn options_map_through() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
polygon_type: Some(CommonPolygonType::Triangle),
|
||||
face_count: Some(40_000),
|
||||
enable_pbr: Some(false),
|
||||
..text_builder()
|
||||
};
|
||||
let text = expect_text(build_fal_hunyuan3d_3_state(builder).expect("build"));
|
||||
assert!(matches!(text.request.generate_type, Some(Hunyuan3d3TextToMeshGenerateType::Geometry)));
|
||||
assert!(matches!(text.request.polygon_type, Some(Hunyuan3d3TextToMeshPolygonType::Triangle)));
|
||||
assert_eq!(text.request.face_count, Some(40_000));
|
||||
assert_eq!(text.request.enable_pbr, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn side_images_without_front_error_out_under_error_out() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::Url(BACK_URL.to_string())),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..text_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan3d_3_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn side_images_without_front_are_dropped_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::Url(BACK_URL.to_string())),
|
||||
..text_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan3d_3_state(builder).expect("build"),
|
||||
FalHunyuan3d3State::Text(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Fal,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn image_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec![FRONT_URL.to_string()])),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn text_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_image(state: FalHunyuan3d3State) -> FalHunyuan3d3ImageRequestState {
|
||||
match state {
|
||||
FalHunyuan3d3State::Image(image) => image,
|
||||
FalHunyuan3d3State::Text(text) => panic!("expected image mode, got text: {text:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_text(state: FalHunyuan3d3State) -> FalHunyuan3d3TextRequestState {
|
||||
match state {
|
||||
FalHunyuan3d3State::Text(text) => text,
|
||||
FalHunyuan3d3State::Image(image) => panic!("expected text mode, got image: {image:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::request::{
|
||||
FalHunyuan3d3ImageRequestState, FalHunyuan3d3TextRequestState,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d3ImageCostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d3ImageCostState {
|
||||
pub fn from_request(request: &FalHunyuan3d3ImageRequestState) -> Self {
|
||||
// Cost math is owned by fal_client's per-endpoint
|
||||
// `FalRequestCostCalculator` implementations. The router state just
|
||||
// forwards the result so router cost ≡ fal_client cost by construction.
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
fal_mesh_cost_estimate(self.cost_in_usd_cents)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d3TextCostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d3TextCostState {
|
||||
pub fn from_request(request: &FalHunyuan3d3TextRequestState) -> Self {
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
fal_mesh_cost_estimate(self.cost_in_usd_cents)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fal_mesh_cost_estimate(cost_in_usd_cents: u64) -> MeshGenerationCostEstimate {
|
||||
MeshGenerationCostEstimate {
|
||||
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,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
const FRONT_URL: &str = "https://example.com/front.png";
|
||||
|
||||
mod image_mode_costs {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn base_cost_table() {
|
||||
let cases = [
|
||||
(None, 38),
|
||||
(Some(CommonMeshOutputType::Normal), 38),
|
||||
(Some(CommonMeshOutputType::LowPoly), 45),
|
||||
(Some(CommonMeshOutputType::Geometry), 23),
|
||||
];
|
||||
for (output_type, expected) in cases {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: output_type,
|
||||
..image_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), expected, "for {output_type:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_ons_stack() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
enable_pbr: Some(true),
|
||||
face_count: Some(100_000),
|
||||
back_image: Some(ImageRef::Url("https://example.com/back.png".to_string())),
|
||||
..image_builder()
|
||||
};
|
||||
// LowPoly(45) + PBR(15) + face_count(15) + multi_view(15)
|
||||
assert_eq!(estimate_usd_cents(builder), 90);
|
||||
}
|
||||
}
|
||||
|
||||
mod text_mode_costs {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn base_cost_table() {
|
||||
let cases = [
|
||||
(None, 38),
|
||||
(Some(CommonMeshOutputType::Normal), 38),
|
||||
(Some(CommonMeshOutputType::LowPoly), 45),
|
||||
(Some(CommonMeshOutputType::Geometry), 23),
|
||||
];
|
||||
for (output_type, expected) in cases {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: output_type,
|
||||
..text_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), expected, "for {output_type:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_ons_stack() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
enable_pbr: Some(true),
|
||||
face_count: Some(40_000),
|
||||
..text_builder()
|
||||
};
|
||||
// Geometry(23) + PBR(15) + face_count(15)
|
||||
assert_eq!(estimate_usd_cents(builder), 53);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn image_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Fal,
|
||||
reference_images: Some(ImageListRef::Urls(vec![FRONT_URL.to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn text_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Fal,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::api::mesh::image::hunyuan3d_3_image_to_mesh::api::Hunyuan3d3ImageToMeshRequest;
|
||||
use fal_client::requests::api::mesh::text::hunyuan3d_3_text_to_mesh::api::Hunyuan3d3TextToMeshRequest;
|
||||
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::{
|
||||
FalMeshResponsePayload, GenerateMeshResponse,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d3ImageRequestState {
|
||||
/// Final materialized request; ready to fire.
|
||||
pub request: Hunyuan3d3ImageToMeshRequest,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d3ImageRequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_fal_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d3TextRequestState {
|
||||
/// Final materialized request; ready to fire.
|
||||
pub request: Hunyuan3d3TextToMeshRequest,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d3TextRequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_fal_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a mesh request to fal using the client's dispatch mode (webhook or
|
||||
/// queue). Shared by all Fal mesh request states.
|
||||
pub(crate) async fn send_fal_mesh_request<R>(
|
||||
request: &R,
|
||||
client: &RouterFalClient,
|
||||
) -> Result<GenerateMeshResponse, ArtcraftRouterError>
|
||||
where
|
||||
R: FalEndpoint + Clone + Debug + Send + Sync + 'static,
|
||||
{
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
|
||||
let payload = if let Some(webhook_url) = &client.webhook_url {
|
||||
let response = request.send_webhook_request(&client.api_key, webhook_url).await?;
|
||||
FalMeshResponsePayload {
|
||||
request_id: response.request_id,
|
||||
gateway_request_id: response.gateway_request_id,
|
||||
maybe_status_url: None,
|
||||
maybe_response_url: None,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}
|
||||
} else {
|
||||
let response = request.send_queue_request(&client.api_key).await?;
|
||||
FalMeshResponsePayload {
|
||||
request_id: Some(response.request_id),
|
||||
gateway_request_id: None,
|
||||
maybe_status_url: Some(response.status_url),
|
||||
maybe_response_url: Some(response.response_url),
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(GenerateMeshResponse::Fal(payload))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_hunyuan3d_3_image_to_mesh() {
|
||||
let response = run_pipeline(GenerateMeshRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec![ERNEST_SCARED_STUPID_IMAGE_URL.to_string()])),
|
||||
..hunyuan3d_3_builder()
|
||||
}).await;
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_hunyuan3d_3_text_to_mesh() {
|
||||
let response = run_pipeline(GenerateMeshRequestBuilder {
|
||||
prompt: Some("A velociraptor with an open mouth full of sharp teeth.".to_string()),
|
||||
..hunyuan3d_3_builder()
|
||||
}).await;
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn hunyuan3d_3_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Fal,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_fal_client() -> RouterClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
let api_key = FalApiKey::from_str(secret.trim());
|
||||
let webhook_url = "https://example.com/fal-webhook-test".to_string();
|
||||
RouterClient::Fal(RouterFalClient::new_with_webhook(api_key, webhook_url))
|
||||
}
|
||||
|
||||
pub(crate) async fn run_pipeline(builder: GenerateMeshRequestBuilder) -> GenerateMeshResponse {
|
||||
let client = get_fal_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
MeshGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Fal mesh skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
println!("response: {:?}", response);
|
||||
response
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
use fal_client::requests::api::mesh::sketch::hunyuan3d_3_sketch_to_mesh::api::Hunyuan3d3SketchToMeshRequest;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3_sketch::request::FalHunyuan3d3SketchRequestState;
|
||||
use crate::generate::generate_mesh::providers::fal::resolve::{
|
||||
plan_face_count, plan_primary_image_url,
|
||||
};
|
||||
use crate::generate::generate_mesh::providers::reject_unsupported::{
|
||||
reject_unsupported_image_ref, reject_unsupported_option,
|
||||
};
|
||||
|
||||
pub fn build_fal_hunyuan3d_3_sketch(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_fal_hunyuan3d_3_sketch_state(builder)?;
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::FalHunyuan3d3Sketch(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn build_fal_hunyuan3d_3_sketch_state(
|
||||
mut builder: GenerateMeshRequestBuilder,
|
||||
) -> Result<FalHunyuan3d3SketchRequestState, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// fal's sketch-to-3d schema has no generate_type/polygon_type parameters,
|
||||
// and no multi-view side images.
|
||||
reject_unsupported_option("mesh_output_type", builder.mesh_output_type.as_ref(), strategy)?;
|
||||
reject_unsupported_option("polygon_type", builder.polygon_type.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("back_image", builder.back_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("left_image", builder.left_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("right_image", builder.right_image.as_ref(), strategy)?;
|
||||
|
||||
let image_url = plan_primary_image_url(
|
||||
builder.reference_images.take(),
|
||||
builder.front_image.take(),
|
||||
strategy,
|
||||
)?
|
||||
.ok_or_else(|| ArtcraftRouterError::InvalidInput(
|
||||
"A sketch image is required for Hunyuan 3D v3 sketch-to-3D".to_string(),
|
||||
))?;
|
||||
|
||||
let prompt = builder.prompt.take().ok_or_else(|| {
|
||||
ArtcraftRouterError::InvalidInput(
|
||||
"A prompt is required for Hunyuan 3D v3 sketch-to-3D".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let request = Hunyuan3d3SketchToMeshRequest {
|
||||
image_url,
|
||||
prompt,
|
||||
face_count: plan_face_count(builder.face_count, strategy)?,
|
||||
enable_pbr: builder.enable_pbr,
|
||||
};
|
||||
|
||||
Ok(FalHunyuan3d3SketchRequestState { request })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
use enums::common::generation::common_polygon_type::CommonPolygonType;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
|
||||
use super::*;
|
||||
|
||||
const SKETCH_URL: &str = "https://example.com/sketch.png";
|
||||
|
||||
#[test]
|
||||
fn sketch_and_prompt_pass_through() {
|
||||
let state = build_fal_hunyuan3d_3_sketch_state(base_builder()).expect("build");
|
||||
assert_eq!(state.request.image_url, SKETCH_URL);
|
||||
assert_eq!(state.request.prompt, "a red ceramic teapot");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_sketch_is_rejected() {
|
||||
let builder = GenerateMeshRequestBuilder { reference_images: None, ..base_builder() };
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan3d_3_sketch_state(builder),
|
||||
Err(ArtcraftRouterError::InvalidInput(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_is_rejected() {
|
||||
let builder = GenerateMeshRequestBuilder { prompt: None, ..base_builder() };
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan3d_3_sketch_state(builder),
|
||||
Err(ArtcraftRouterError::InvalidInput(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_count_and_pbr_pass_through() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
face_count: Some(100_000),
|
||||
enable_pbr: Some(true),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan3d_3_sketch_state(builder).expect("build");
|
||||
assert_eq!(state.request.face_count, Some(100_000));
|
||||
assert_eq!(state.request.enable_pbr, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_error_out() {
|
||||
let cases: Vec<GenerateMeshRequestBuilder> = vec![
|
||||
GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
..base_builder()
|
||||
},
|
||||
GenerateMeshRequestBuilder {
|
||||
polygon_type: Some(CommonPolygonType::Quad),
|
||||
..base_builder()
|
||||
},
|
||||
GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::Url("https://example.com/back.png".to_string())),
|
||||
..base_builder()
|
||||
},
|
||||
];
|
||||
for mut builder in cases {
|
||||
builder.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut;
|
||||
assert!(build_fal_hunyuan3d_3_sketch_state(builder).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_are_dropped_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
polygon_type: Some(CommonPolygonType::Quad),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan3d_3_sketch_state(builder).is_ok());
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3Sketch,
|
||||
provider: RouterProvider::Fal,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
reference_images: Some(ImageListRef::Urls(vec![SKETCH_URL.to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::cost::fal_mesh_cost_estimate;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3_sketch::request::FalHunyuan3d3SketchRequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d3SketchCostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d3SketchCostState {
|
||||
pub fn from_request(request: &FalHunyuan3d3SketchRequestState) -> Self {
|
||||
// Cost math is owned by fal_client's per-endpoint
|
||||
// `FalRequestCostCalculator` implementations. The router state just
|
||||
// forwards the result so router cost ≡ fal_client cost by construction.
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
fal_mesh_cost_estimate(self.cost_in_usd_cents)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
#[test]
|
||||
fn base_cost_is_thirty_eight_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 38);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbr_adds_fifteen_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(true),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 38 + 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_count_adds_fifteen_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
face_count: Some(100_000),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 38 + 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_add_ons_stack() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(true),
|
||||
face_count: Some(100_000),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 38 + 15 + 15);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3Sketch,
|
||||
provider: RouterProvider::Fal,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
reference_images: Some(ImageListRef::Urls(vec!["https://example.com/sketch.png".to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
use fal_client::requests::api::mesh::sketch::hunyuan3d_3_sketch_to_mesh::api::Hunyuan3d3SketchToMeshRequest;
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::request::send_fal_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d3SketchRequestState {
|
||||
/// Final materialized request; ready to fire.
|
||||
pub request: Hunyuan3d3SketchToMeshRequest,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d3SketchRequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_fal_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_sketch_to_mesh() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3Sketch,
|
||||
provider: RouterProvider::Fal,
|
||||
prompt: Some("A cartoon character, colorful, plastic material".to_string()),
|
||||
reference_images: Some(ImageListRef::Urls(vec![ERNEST_SCARED_STUPID_IMAGE_URL.to_string()])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let client = get_fal_client();
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
MeshGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Fal mesh skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
println!("response: {:?}", response);
|
||||
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn get_fal_client() -> RouterClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
let api_key = FalApiKey::from_str(secret.trim());
|
||||
let webhook_url = "https://example.com/fal-webhook-test".to_string();
|
||||
RouterClient::Fal(RouterFalClient::new_with_webhook(api_key, webhook_url))
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
use fal_client::requests::api::mesh::image::hunyuan_3d_2p0_image_to_mesh::api::Hunyuan3d2p0ImageToMeshRequest;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_2p0::request::FalHunyuan3d2p0RequestState;
|
||||
use crate::generate::generate_mesh::providers::fal::resolve::{
|
||||
plan_primary_image_url, plan_textured_mesh,
|
||||
};
|
||||
use crate::generate::generate_mesh::providers::reject_unsupported::{
|
||||
reject_unsupported_image_ref, reject_unsupported_option,
|
||||
};
|
||||
|
||||
pub fn build_fal_hunyuan_3d_2p0(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_fal_hunyuan_3d_2p0_state(builder)?;
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::FalHunyuan3d2p0(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn build_fal_hunyuan_3d_2p0_state(
|
||||
mut builder: GenerateMeshRequestBuilder,
|
||||
) -> Result<FalHunyuan3d2p0RequestState, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Hunyuan 3D 2.0 is image-to-3D only, with a single input image and no
|
||||
// extra mesh options.
|
||||
reject_unsupported_option("prompt", builder.prompt.as_ref(), strategy)?;
|
||||
reject_unsupported_option("polygon_type", builder.polygon_type.as_ref(), strategy)?;
|
||||
reject_unsupported_option("face_count", builder.face_count.as_ref(), strategy)?;
|
||||
reject_unsupported_option("enable_pbr", builder.enable_pbr.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("back_image", builder.back_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("left_image", builder.left_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("right_image", builder.right_image.as_ref(), strategy)?;
|
||||
|
||||
let image_url = plan_primary_image_url(
|
||||
builder.reference_images.take(),
|
||||
builder.front_image.take(),
|
||||
strategy,
|
||||
)?
|
||||
.ok_or_else(|| ArtcraftRouterError::InvalidInput(
|
||||
"An input image is required for Hunyuan 3D 2.0".to_string(),
|
||||
))?;
|
||||
|
||||
let request = Hunyuan3d2p0ImageToMeshRequest {
|
||||
image_url,
|
||||
textured_mesh: plan_textured_mesh(builder.mesh_output_type, strategy)?,
|
||||
// Tuning knobs stay at the endpoint's defaults.
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
octree_resolution: None,
|
||||
seed: None,
|
||||
};
|
||||
|
||||
Ok(FalHunyuan3d2p0RequestState { request })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
use enums::common::generation::common_polygon_type::CommonPolygonType;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
|
||||
use super::*;
|
||||
|
||||
const IMAGE_URL: &str = "https://example.com/front.png";
|
||||
|
||||
mod image_requirements {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn single_image_passes_through() {
|
||||
let state = build_fal_hunyuan_3d_2p0_state(base_builder()).expect("build");
|
||||
assert_eq!(state.request.image_url, IMAGE_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_image_is_rejected() {
|
||||
let builder = GenerateMeshRequestBuilder { reference_images: None, ..base_builder() };
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan_3d_2p0_state(builder),
|
||||
Err(ArtcraftRouterError::InvalidInput(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_images_error_out() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec![
|
||||
IMAGE_URL.to_string(),
|
||||
"https://example.com/other.png".to_string(),
|
||||
])),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan_3d_2p0_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_images_keep_the_first_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec![
|
||||
IMAGE_URL.to_string(),
|
||||
"https://example.com/other.png".to_string(),
|
||||
])),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan_3d_2p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.image_url, IMAGE_URL);
|
||||
}
|
||||
}
|
||||
|
||||
mod output_type_mapping {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_maps_to_textured() {
|
||||
let state = build_fal_hunyuan_3d_2p0_state(base_builder()).expect("build");
|
||||
assert_eq!(state.request.textured_mesh, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_maps_to_textured() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Normal),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan_3d_2p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.textured_mesh, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_maps_to_white_mesh() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan_3d_2p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.textured_mesh, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_poly_errors_out_under_error_out() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan_3d_2p0_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_poly_upgrades_to_textured_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan_3d_2p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.textured_mesh, Some(true));
|
||||
}
|
||||
}
|
||||
|
||||
mod unsupported_options {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_error_out() {
|
||||
let cases: Vec<GenerateMeshRequestBuilder> = vec![
|
||||
GenerateMeshRequestBuilder { prompt: Some("a teapot".to_string()), ..base_builder() },
|
||||
GenerateMeshRequestBuilder { polygon_type: Some(CommonPolygonType::Quad), ..base_builder() },
|
||||
GenerateMeshRequestBuilder { face_count: Some(100_000), ..base_builder() },
|
||||
GenerateMeshRequestBuilder { enable_pbr: Some(true), ..base_builder() },
|
||||
GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::Url("https://example.com/back.png".to_string())),
|
||||
..base_builder()
|
||||
},
|
||||
];
|
||||
for mut builder in cases {
|
||||
builder.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut;
|
||||
assert!(build_fal_hunyuan_3d_2p0_state(builder).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_are_dropped_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
prompt: Some("a teapot".to_string()),
|
||||
face_count: Some(100_000),
|
||||
enable_pbr: Some(true),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan_3d_2p0_state(builder).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuning_knobs_stay_at_endpoint_defaults() {
|
||||
let state = build_fal_hunyuan_3d_2p0_state(base_builder()).expect("build");
|
||||
assert!(state.request.guidance_scale.is_none());
|
||||
assert!(state.request.num_inference_steps.is_none());
|
||||
assert!(state.request.octree_resolution.is_none());
|
||||
assert!(state.request.seed.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p0,
|
||||
provider: RouterProvider::Fal,
|
||||
reference_images: Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::cost::fal_mesh_cost_estimate;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_2p0::request::FalHunyuan3d2p0RequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d2p0CostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d2p0CostState {
|
||||
pub fn from_request(request: &FalHunyuan3d2p0RequestState) -> Self {
|
||||
// Cost math is owned by fal_client's per-endpoint
|
||||
// `FalRequestCostCalculator` implementations. The router state just
|
||||
// forwards the result so router cost ≡ fal_client cost by construction.
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
fal_mesh_cost_estimate(self.cost_in_usd_cents)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
#[test]
|
||||
fn textured_mesh_is_forty_eight_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 48);
|
||||
|
||||
let normal = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Normal),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(normal), 48);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_mesh_is_sixteen_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 16);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p0,
|
||||
provider: RouterProvider::Fal,
|
||||
reference_images: Some(ImageListRef::Urls(vec!["https://example.com/front.png".to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
use fal_client::requests::api::mesh::image::hunyuan_3d_2p0_image_to_mesh::api::Hunyuan3d2p0ImageToMeshRequest;
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::request::send_fal_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d2p0RequestState {
|
||||
/// Final materialized request; ready to fire.
|
||||
pub request: Hunyuan3d2p0ImageToMeshRequest,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d2p0RequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_fal_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_hunyuan_3d_2p0_image_to_mesh() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p0,
|
||||
provider: RouterProvider::Fal,
|
||||
reference_images: Some(ImageListRef::Urls(vec![ERNEST_SCARED_STUPID_IMAGE_URL.to_string()])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let client = get_fal_client();
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
MeshGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Fal mesh skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
println!("response: {:?}", response);
|
||||
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn get_fal_client() -> RouterClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
let api_key = FalApiKey::from_str(secret.trim());
|
||||
let webhook_url = "https://example.com/fal-webhook-test".to_string();
|
||||
RouterClient::Fal(RouterFalClient::new_with_webhook(api_key, webhook_url))
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
use fal_client::requests::api::mesh::image::hunyuan_3d_2p1_image_to_mesh::api::Hunyuan3d2p1ImageToMeshRequest;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_2p1::request::FalHunyuan3d2p1RequestState;
|
||||
use crate::generate::generate_mesh::providers::fal::resolve::{
|
||||
plan_primary_image_url, plan_textured_mesh,
|
||||
};
|
||||
use crate::generate::generate_mesh::providers::reject_unsupported::{
|
||||
reject_unsupported_image_ref, reject_unsupported_option,
|
||||
};
|
||||
|
||||
pub fn build_fal_hunyuan_3d_2p1(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_fal_hunyuan_3d_2p1_state(builder)?;
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::FalHunyuan3d2p1(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn build_fal_hunyuan_3d_2p1_state(
|
||||
mut builder: GenerateMeshRequestBuilder,
|
||||
) -> Result<FalHunyuan3d2p1RequestState, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Hunyuan 3D 2.1 is image-to-3D only, with a single input image and no
|
||||
// extra mesh options.
|
||||
reject_unsupported_option("prompt", builder.prompt.as_ref(), strategy)?;
|
||||
reject_unsupported_option("polygon_type", builder.polygon_type.as_ref(), strategy)?;
|
||||
reject_unsupported_option("face_count", builder.face_count.as_ref(), strategy)?;
|
||||
reject_unsupported_option("enable_pbr", builder.enable_pbr.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("back_image", builder.back_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("left_image", builder.left_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("right_image", builder.right_image.as_ref(), strategy)?;
|
||||
|
||||
let image_url = plan_primary_image_url(
|
||||
builder.reference_images.take(),
|
||||
builder.front_image.take(),
|
||||
strategy,
|
||||
)?
|
||||
.ok_or_else(|| ArtcraftRouterError::InvalidInput(
|
||||
"An input image is required for Hunyuan 3D 2.1".to_string(),
|
||||
))?;
|
||||
|
||||
let request = Hunyuan3d2p1ImageToMeshRequest {
|
||||
image_url,
|
||||
textured_mesh: plan_textured_mesh(builder.mesh_output_type, strategy)?,
|
||||
// Tuning knobs stay at the endpoint's defaults.
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
octree_resolution: None,
|
||||
seed: None,
|
||||
};
|
||||
|
||||
Ok(FalHunyuan3d2p1RequestState { request })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
|
||||
use super::*;
|
||||
|
||||
const IMAGE_URL: &str = "https://example.com/front.png";
|
||||
|
||||
#[test]
|
||||
fn single_image_passes_through() {
|
||||
let state = build_fal_hunyuan_3d_2p1_state(base_builder()).expect("build");
|
||||
assert_eq!(state.request.image_url, IMAGE_URL);
|
||||
assert_eq!(state.request.textured_mesh, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_image_is_rejected() {
|
||||
let builder = GenerateMeshRequestBuilder { reference_images: None, ..base_builder() };
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan_3d_2p1_state(builder),
|
||||
Err(ArtcraftRouterError::InvalidInput(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_maps_to_white_mesh() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan_3d_2p1_state(builder).expect("build");
|
||||
assert_eq!(state.request.textured_mesh, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_poly_errors_out_under_error_out() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan_3d_2p1_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_poly_upgrades_to_textured_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan_3d_2p1_state(builder).expect("build");
|
||||
assert_eq!(state.request.textured_mesh, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_error_out() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
prompt: Some("a teapot".to_string()),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan_3d_2p1_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_are_dropped_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
prompt: Some("a teapot".to_string()),
|
||||
face_count: Some(100_000),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan_3d_2p1_state(builder).is_ok());
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p1,
|
||||
provider: RouterProvider::Fal,
|
||||
reference_images: Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::cost::fal_mesh_cost_estimate;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_2p1::request::FalHunyuan3d2p1RequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d2p1CostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d2p1CostState {
|
||||
pub fn from_request(request: &FalHunyuan3d2p1RequestState) -> Self {
|
||||
// Cost math is owned by fal_client's per-endpoint
|
||||
// `FalRequestCostCalculator` implementations. The router state just
|
||||
// forwards the result so router cost ≡ fal_client cost by construction.
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
fal_mesh_cost_estimate(self.cost_in_usd_cents)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
#[test]
|
||||
fn textured_mesh_is_ninety_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 90);
|
||||
|
||||
let normal = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Normal),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(normal), 90);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_mesh_is_thirty_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 30);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p1,
|
||||
provider: RouterProvider::Fal,
|
||||
reference_images: Some(ImageListRef::Urls(vec!["https://example.com/front.png".to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
use fal_client::requests::api::mesh::image::hunyuan_3d_2p1_image_to_mesh::api::Hunyuan3d2p1ImageToMeshRequest;
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::request::send_fal_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d2p1RequestState {
|
||||
/// Final materialized request; ready to fire.
|
||||
pub request: Hunyuan3d2p1ImageToMeshRequest,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d2p1RequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_fal_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_hunyuan_3d_2p1_image_to_mesh() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p1,
|
||||
provider: RouterProvider::Fal,
|
||||
reference_images: Some(ImageListRef::Urls(vec![ERNEST_SCARED_STUPID_IMAGE_URL.to_string()])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let client = get_fal_client();
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
MeshGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Fal mesh skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
println!("response: {:?}", response);
|
||||
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn get_fal_client() -> RouterClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
let api_key = FalApiKey::from_str(secret.trim());
|
||||
let webhook_url = "https://example.com/fal-webhook-test".to_string();
|
||||
RouterClient::Fal(RouterFalClient::new_with_webhook(api_key, webhook_url))
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
pub(crate) mod resolve;
|
||||
pub mod hunyuan3d_3;
|
||||
pub mod hunyuan3d_3_sketch;
|
||||
pub mod hunyuan_3d_2p0;
|
||||
pub mod hunyuan_3d_2p1;
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
//! Shared plan helpers for the Fal mesh provider.
|
||||
//!
|
||||
//! Fal only accepts media URLs — media file tokens are rejected. Callers
|
||||
//! must resolve tokens to URLs before building for the Fal provider.
|
||||
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
/// Resolve the primary (front) image from `reference_images` and/or
|
||||
/// `front_image`. More than one candidate rejects under `ErrorOut`; the other
|
||||
/// strategies keep the first (reference images take precedence over
|
||||
/// `front_image`).
|
||||
pub(crate) fn plan_primary_image_url(
|
||||
reference_images: Option<ImageListRef>,
|
||||
front_image: Option<ImageRef>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
let mut candidates: Vec<String> = Vec::new();
|
||||
|
||||
match reference_images {
|
||||
None => {}
|
||||
Some(ImageListRef::Urls(urls)) => candidates.extend(urls),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) if tokens.is_empty() => {}
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(front) = front_image {
|
||||
candidates.push(image_ref_to_url(front)?);
|
||||
}
|
||||
|
||||
if candidates.len() > 1 {
|
||||
if let RequestMismatchMitigationStrategy::ErrorOut = strategy {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "reference_images",
|
||||
value: format!("Expected at most 1 primary image, got {}", candidates.len()),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(candidates.into_iter().next())
|
||||
}
|
||||
|
||||
/// Resolve an optional side-view image (back/left/right) to a URL.
|
||||
pub(crate) fn plan_side_image_url(
|
||||
image_ref: Option<ImageRef>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
image_ref.map(image_ref_to_url).transpose()
|
||||
}
|
||||
|
||||
/// Narrow a `u64` face count to the `u32` the fal bindings take.
|
||||
/// Out-of-range values reject under `ErrorOut` and clamp otherwise.
|
||||
pub(crate) fn plan_face_count(
|
||||
face_count: Option<u64>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u32>, ArtcraftRouterError> {
|
||||
let Some(face_count) = face_count else {
|
||||
return Ok(None);
|
||||
};
|
||||
match u32::try_from(face_count) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(_) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "face_count",
|
||||
value: format!("{}", face_count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(u32::MAX)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a mesh output type onto the Hunyuan 2.x endpoints' `textured_mesh`
|
||||
/// flag. Those endpoints only produce textured or white (geometry-only)
|
||||
/// meshes: `Normal` (or unset) maps to textured, `Geometry` to white, and
|
||||
/// `LowPoly` rejects under `ErrorOut` or upgrades to textured otherwise.
|
||||
pub(crate) fn plan_textured_mesh(
|
||||
mesh_output_type: Option<CommonMeshOutputType>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<bool>, ArtcraftRouterError> {
|
||||
match mesh_output_type {
|
||||
None | Some(CommonMeshOutputType::Normal) => Ok(Some(true)),
|
||||
Some(CommonMeshOutputType::Geometry) => Ok(Some(false)),
|
||||
Some(CommonMeshOutputType::LowPoly) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "mesh_output_type",
|
||||
value: format!("{:?}", CommonMeshOutputType::LowPoly),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(true)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn image_ref_to_url(image_ref: ImageRef) -> Result<String, ArtcraftRouterError> {
|
||||
match image_ref {
|
||||
ImageRef::Url(url) => Ok(url),
|
||||
ImageRef::MediaFileToken(_) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod primary_image_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn none_resolves_to_none() {
|
||||
let result = plan_primary_image_url(None, None, RequestMismatchMitigationStrategy::ErrorOut)
|
||||
.expect("plan");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_reference_url_is_used() {
|
||||
let refs = Some(ImageListRef::Urls(vec!["https://example.com/a.png".to_string()]));
|
||||
let result = plan_primary_image_url(refs, None, RequestMismatchMitigationStrategy::ErrorOut)
|
||||
.expect("plan");
|
||||
assert_eq!(result.as_deref(), Some("https://example.com/a.png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn front_image_is_used_when_no_references() {
|
||||
let front = Some(ImageRef::Url("https://example.com/front.png".to_string()));
|
||||
let result = plan_primary_image_url(None, front, RequestMismatchMitigationStrategy::ErrorOut)
|
||||
.expect("plan");
|
||||
assert_eq!(result.as_deref(), Some("https://example.com/front.png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_candidates_error_out() {
|
||||
let refs = Some(ImageListRef::Urls(vec!["https://example.com/a.png".to_string()]));
|
||||
let front = Some(ImageRef::Url("https://example.com/front.png".to_string()));
|
||||
let result = plan_primary_image_url(refs, front, RequestMismatchMitigationStrategy::ErrorOut);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_candidates_keep_the_first_under_lenient_strategies() {
|
||||
let refs = Some(ImageListRef::Urls(vec![
|
||||
"https://example.com/a.png".to_string(),
|
||||
"https://example.com/b.png".to_string(),
|
||||
]));
|
||||
let result = plan_primary_image_url(refs, None, RequestMismatchMitigationStrategy::PayMoreUpgrade)
|
||||
.expect("plan");
|
||||
assert_eq!(result.as_deref(), Some("https://example.com/a.png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_tokens_are_rejected() {
|
||||
let refs = Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_test123".to_string()),
|
||||
]));
|
||||
let result = plan_primary_image_url(refs, None, RequestMismatchMitigationStrategy::PayMoreUpgrade);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_token_list_resolves_to_none() {
|
||||
let refs = Some(ImageListRef::MediaFileTokens(vec![]));
|
||||
let result = plan_primary_image_url(refs, None, RequestMismatchMitigationStrategy::ErrorOut)
|
||||
.expect("plan");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod face_count_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn in_range_passes_through() {
|
||||
let result = plan_face_count(Some(500_000), RequestMismatchMitigationStrategy::ErrorOut)
|
||||
.expect("plan");
|
||||
assert_eq!(result, Some(500_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_errors_out() {
|
||||
let result = plan_face_count(Some(u64::MAX), RequestMismatchMitigationStrategy::ErrorOut);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_clamps_under_lenient_strategies() {
|
||||
let result = plan_face_count(Some(u64::MAX), RequestMismatchMitigationStrategy::PayLessDowngrade)
|
||||
.expect("plan");
|
||||
assert_eq!(result, Some(u32::MAX));
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod artcraft;
|
||||
pub mod fal;
|
||||
pub(crate) mod reject_unsupported;
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
//! Shared helpers for rejecting builder options that a model does not support.
|
||||
//!
|
||||
//! Per the request-mismatch mitigation strategy: `ErrorOut` rejects the
|
||||
//! request when an unsupported option is set; the other strategies silently
|
||||
//! drop the option.
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
/// Reject (under `ErrorOut`) or silently drop a set option the model doesn't
|
||||
/// support.
|
||||
pub(crate) fn reject_unsupported_option<T: Debug>(
|
||||
field: &'static str,
|
||||
value: Option<&T>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<(), ArtcraftRouterError> {
|
||||
let Some(value) = value else {
|
||||
return Ok(());
|
||||
};
|
||||
match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field,
|
||||
value: format!("{:?}", value),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reject (under `ErrorOut`) or silently drop a single side-view image for
|
||||
/// models/endpoints that take none.
|
||||
pub(crate) fn reject_unsupported_image_ref(
|
||||
field: &'static str,
|
||||
image_ref: Option<&ImageRef>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<(), ArtcraftRouterError> {
|
||||
reject_unsupported_option(field, image_ref, strategy)
|
||||
}
|
||||
+1
@@ -3,6 +3,7 @@ use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenera
|
||||
use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel;
|
||||
use worldlabs_api_client::pricing::check_pricing::{calculate_cost, InputType};
|
||||
|
||||
#[allow(deprecated)] // legacy marble 0.1 pricing intentionally uses the deprecated variants
|
||||
pub(crate) fn estimate_splat_cost_artcraft_marble_0p1_mini(
|
||||
plan: &PlanArtcraftMarble0p1Mini,
|
||||
) -> SplatGenerationCostEstimate {
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenera
|
||||
use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel;
|
||||
use worldlabs_api_client::pricing::check_pricing::{calculate_cost, InputType};
|
||||
|
||||
#[allow(deprecated)] // legacy marble 0.1 pricing intentionally uses the deprecated variants
|
||||
pub(crate) fn estimate_splat_cost_artcraft_marble_0p1_plus(
|
||||
plan: &PlanArtcraftMarble0p1Plus,
|
||||
) -> SplatGenerationCostEstimate {
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ pub async fn execute_artcraft_marble_0p1_mini(
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateSplatResponse::Artcraft(ArtcraftSplatResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
inference_job_token: response.inference_job_token.clone(),
|
||||
all_inference_job_tokens: vec![response.inference_job_token],
|
||||
}))
|
||||
}
|
||||
+2
-1
@@ -27,6 +27,7 @@ pub async fn execute_artcraft_marble_0p1_plus(
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateSplatResponse::Artcraft(ArtcraftSplatResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
inference_job_token: response.inference_job_token.clone(),
|
||||
all_inference_job_tokens: vec![response.inference_job_token],
|
||||
}))
|
||||
}
|
||||
+5
@@ -37,6 +37,11 @@ impl GenerateSplatRequest {
|
||||
match self.model {
|
||||
RouterSplatModel::Marble0p1Mini => plan_generate_splat_artcraft_marble_0p1_mini(self),
|
||||
RouterSplatModel::Marble0p1Plus => plan_generate_splat_artcraft_marble_0p1_plus(self),
|
||||
// The marble 1.x models are only supported by the new
|
||||
// `GenerateSplatRequestBuilder::build2()` pipeline.
|
||||
_ => Err(ArtcraftRouterError::UnsupportedModel(
|
||||
format!("Splat generation for model `{:?}` is not supported by the legacy plan API; use GenerateSplatRequestBuilder::build2()", self.model)
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_splat_model::RouterSplatModel;
|
||||
use crate::api::video_ref::VideoRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::providers::artcraft::marble_1p0::build::build_artcraft_marble_1p0;
|
||||
use crate::generate::generate_splat::providers::artcraft::marble_1p0_draft::build::build_artcraft_marble_1p0_draft;
|
||||
use crate::generate::generate_splat::providers::artcraft::marble_1p1::build::build_artcraft_marble_1p1;
|
||||
use crate::generate::generate_splat::providers::artcraft::marble_1p1_plus::build::build_artcraft_marble_1p1_plus;
|
||||
use crate::generate::generate_splat::providers::worldlabs::marble_1p0::build::build_worldlabs_marble_1p0;
|
||||
use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::build::build_worldlabs_marble_1p0_draft;
|
||||
use crate::generate::generate_splat::providers::worldlabs::marble_1p1::build::build_worldlabs_marble_1p1;
|
||||
use crate::generate::generate_splat::providers::worldlabs::marble_1p1_plus::build::build_worldlabs_marble_1p1_plus;
|
||||
use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest;
|
||||
|
||||
/// RouterProvider-agnostic splat (gaussian world) generation request.
|
||||
/// Distilled by `build2()` into a `SplatGenerationDraftOrRequest` for the
|
||||
/// selected (provider, model) pair.
|
||||
///
|
||||
/// NB: The deprecated marble 0.1 models are treated as their marble 1.x
|
||||
/// successors (`Marble0p1Mini` → `Marble1p0Draft`, `Marble0p1Plus` →
|
||||
/// `Marble1p0`).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateSplatRequestBuilder {
|
||||
/// Which model to use.
|
||||
pub model: RouterSplatModel,
|
||||
|
||||
/// Which provider to use.
|
||||
pub provider: RouterProvider,
|
||||
|
||||
/// The text prompt for the splat generation.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Reference images (optional).
|
||||
/// One image generates from a single view; multiple images are treated as
|
||||
/// multi-view input of the same world.
|
||||
pub reference_images: Option<ImageListRef>,
|
||||
|
||||
/// Reference video (optional). Cannot be combined with reference images.
|
||||
pub reference_video: Option<VideoRef>,
|
||||
|
||||
/// Whether the single reference image is a 360-degree panorama.
|
||||
pub is_panoramic: Option<bool>,
|
||||
|
||||
/// Whether to disable prompt recaptioning.
|
||||
pub disable_recaption: Option<bool>,
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
impl Default for GenerateSplatRequestBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: RouterSplatModel::Marble1p0Draft,
|
||||
provider: RouterProvider::Artcraft,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
prompt: None,
|
||||
reference_images: None,
|
||||
reference_video: None,
|
||||
is_panoramic: None,
|
||||
disable_recaption: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateSplatRequestBuilder {
|
||||
|
||||
pub fn build2(self) -> Result<SplatGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
match (self.provider, self.model) {
|
||||
// Artcraft
|
||||
(RouterProvider::Artcraft, RouterSplatModel::Marble1p0 | RouterSplatModel::Marble0p1Plus) => build_artcraft_marble_1p0(self),
|
||||
(RouterProvider::Artcraft, RouterSplatModel::Marble1p0Draft | RouterSplatModel::Marble0p1Mini) => build_artcraft_marble_1p0_draft(self),
|
||||
(RouterProvider::Artcraft, RouterSplatModel::Marble1p1) => build_artcraft_marble_1p1(self),
|
||||
(RouterProvider::Artcraft, RouterSplatModel::Marble1p1Plus) => build_artcraft_marble_1p1_plus(self),
|
||||
// World Labs
|
||||
(RouterProvider::WorldLabs, RouterSplatModel::Marble1p0 | RouterSplatModel::Marble0p1Plus) => build_worldlabs_marble_1p0(self),
|
||||
(RouterProvider::WorldLabs, RouterSplatModel::Marble1p0Draft | RouterSplatModel::Marble0p1Mini) => build_worldlabs_marble_1p0_draft(self),
|
||||
(RouterProvider::WorldLabs, RouterSplatModel::Marble1p1) => build_worldlabs_marble_1p1(self),
|
||||
(RouterProvider::WorldLabs, RouterSplatModel::Marble1p1Plus) => build_worldlabs_marble_1p1_plus(self),
|
||||
_ => self.unsupported_provider_and_model(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_generate_idempotency_token(&self) -> String {
|
||||
self.idempotency_token.clone()
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
|
||||
}
|
||||
|
||||
fn unsupported_provider_and_model(&self) -> Result<SplatGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(
|
||||
format!("Splat generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::generate::generate_splat::splat_generation_draft::SplatGenerationDraftRequest;
|
||||
use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
const IMAGE_URL: &str = "https://example.com/room.png";
|
||||
|
||||
mod artcraft_dispatch_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_models_dispatch_to_requests() {
|
||||
let cases = [
|
||||
RouterSplatModel::Marble1p0,
|
||||
RouterSplatModel::Marble1p0Draft,
|
||||
RouterSplatModel::Marble1p1,
|
||||
RouterSplatModel::Marble1p1Plus,
|
||||
];
|
||||
for model in cases {
|
||||
let result = artcraft_builder(model).build2()
|
||||
.unwrap_or_else(|e| panic!("build should succeed for {model:?}: {e}"));
|
||||
let request = expect_request(result);
|
||||
let matches = matches!(
|
||||
(model, &request),
|
||||
(RouterSplatModel::Marble1p0, SplatGenerationRequest::ArtcraftMarble1p0(_))
|
||||
| (RouterSplatModel::Marble1p0Draft, SplatGenerationRequest::ArtcraftMarble1p0Draft(_))
|
||||
| (RouterSplatModel::Marble1p1, SplatGenerationRequest::ArtcraftMarble1p1(_))
|
||||
| (RouterSplatModel::Marble1p1Plus, SplatGenerationRequest::ArtcraftMarble1p1Plus(_))
|
||||
);
|
||||
assert!(matches, "unexpected dispatch for {model:?}: {request:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_marble_0p1_mini_is_treated_as_marble_1p0_draft() {
|
||||
let result = artcraft_builder(RouterSplatModel::Marble0p1Mini).build2().expect("build");
|
||||
assert!(matches!(
|
||||
expect_request(result),
|
||||
SplatGenerationRequest::ArtcraftMarble1p0Draft(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_marble_0p1_plus_is_treated_as_marble_1p0() {
|
||||
let result = artcraft_builder(RouterSplatModel::Marble0p1Plus).build2().expect("build");
|
||||
assert!(matches!(
|
||||
expect_request(result),
|
||||
SplatGenerationRequest::ArtcraftMarble1p0(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
mod worldlabs_dispatch_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn text_prompts_dispatch_to_direct_requests() {
|
||||
let cases = [
|
||||
RouterSplatModel::Marble1p0,
|
||||
RouterSplatModel::Marble1p0Draft,
|
||||
RouterSplatModel::Marble1p1,
|
||||
RouterSplatModel::Marble1p1Plus,
|
||||
];
|
||||
for model in cases {
|
||||
let result = worldlabs_text_builder(model).build2()
|
||||
.unwrap_or_else(|e| panic!("build should succeed for {model:?}: {e}"));
|
||||
let request = expect_request(result);
|
||||
let matches = matches!(
|
||||
(model, &request),
|
||||
(RouterSplatModel::Marble1p0, SplatGenerationRequest::WorldLabsMarble1p0(_))
|
||||
| (RouterSplatModel::Marble1p0Draft, SplatGenerationRequest::WorldLabsMarble1p0Draft(_))
|
||||
| (RouterSplatModel::Marble1p1, SplatGenerationRequest::WorldLabsMarble1p1(_))
|
||||
| (RouterSplatModel::Marble1p1Plus, SplatGenerationRequest::WorldLabsMarble1p1Plus(_))
|
||||
);
|
||||
assert!(matches, "unexpected dispatch for {model:?}: {request:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_inputs_dispatch_to_drafts() {
|
||||
let cases = [
|
||||
RouterSplatModel::Marble1p0,
|
||||
RouterSplatModel::Marble1p0Draft,
|
||||
RouterSplatModel::Marble1p1,
|
||||
RouterSplatModel::Marble1p1Plus,
|
||||
];
|
||||
for model in cases {
|
||||
let builder = GenerateSplatRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])),
|
||||
..worldlabs_text_builder(model)
|
||||
};
|
||||
let result = builder.build2()
|
||||
.unwrap_or_else(|e| panic!("build should succeed for {model:?}: {e}"));
|
||||
let draft = expect_draft(result);
|
||||
let matches = matches!(
|
||||
(model, &draft),
|
||||
(RouterSplatModel::Marble1p0, SplatGenerationDraftRequest::WorldLabsMarble1p0(_))
|
||||
| (RouterSplatModel::Marble1p0Draft, SplatGenerationDraftRequest::WorldLabsMarble1p0Draft(_))
|
||||
| (RouterSplatModel::Marble1p1, SplatGenerationDraftRequest::WorldLabsMarble1p1(_))
|
||||
| (RouterSplatModel::Marble1p1Plus, SplatGenerationDraftRequest::WorldLabsMarble1p1Plus(_))
|
||||
);
|
||||
assert!(matches, "unexpected dispatch for {model:?}: {draft:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_models_are_treated_as_their_successors() {
|
||||
let mini = worldlabs_text_builder(RouterSplatModel::Marble0p1Mini).build2().expect("build");
|
||||
assert!(matches!(
|
||||
expect_request(mini),
|
||||
SplatGenerationRequest::WorldLabsMarble1p0Draft(_)
|
||||
));
|
||||
|
||||
let plus = worldlabs_text_builder(RouterSplatModel::Marble0p1Plus).build2().expect("build");
|
||||
assert!(matches!(
|
||||
expect_request(plus),
|
||||
SplatGenerationRequest::WorldLabsMarble1p0(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_token_inputs_dispatch_to_drafts() {
|
||||
let builder = GenerateSplatRequestBuilder {
|
||||
prompt: None,
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_test123".to_string()),
|
||||
])),
|
||||
..worldlabs_text_builder(RouterSplatModel::Marble1p0)
|
||||
};
|
||||
let result = builder.build2().expect("build");
|
||||
assert!(matches!(result, SplatGenerationDraftOrRequest::Draft(_)));
|
||||
}
|
||||
}
|
||||
|
||||
mod unsupported_combo_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn non_splat_providers_are_unsupported() {
|
||||
for provider in [
|
||||
RouterProvider::Fal,
|
||||
RouterProvider::GmiCloud,
|
||||
RouterProvider::GrokApi,
|
||||
RouterProvider::Seedance2Pro,
|
||||
] {
|
||||
let result = GenerateSplatRequestBuilder {
|
||||
provider,
|
||||
model: RouterSplatModel::Marble1p0,
|
||||
prompt: Some("a cozy cabin".to_string()),
|
||||
..Default::default()
|
||||
}.build2();
|
||||
assert!(
|
||||
matches!(result, Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(_))),
|
||||
"expected unsupported error for {provider:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn artcraft_builder(model: RouterSplatModel) -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
provider: RouterProvider::Artcraft,
|
||||
model,
|
||||
prompt: Some("a cozy cabin in the snowy mountains".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn worldlabs_text_builder(model: RouterSplatModel) -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
provider: RouterProvider::WorldLabs,
|
||||
model,
|
||||
prompt: Some("a cozy cabin in the snowy mountains".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_request(result: SplatGenerationDraftOrRequest) -> SplatGenerationRequest {
|
||||
match result {
|
||||
SplatGenerationDraftOrRequest::Request(request) => request,
|
||||
SplatGenerationDraftOrRequest::Draft(draft) => panic!("expected Request, got Draft: {draft:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_draft(result: SplatGenerationDraftOrRequest) -> SplatGenerationDraftRequest {
|
||||
match result {
|
||||
SplatGenerationDraftOrRequest::Draft(draft) => draft,
|
||||
SplatGenerationDraftOrRequest::Request(request) => panic!("expected Draft, got Request: {request:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -3,17 +3,34 @@ use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSplatResponsePayload {
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
pub all_inference_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WorldLabsSplatResponsePayload {
|
||||
/// The World Labs operation ID; poll it for generation status.
|
||||
pub operation_id: String,
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GenerateSplatResponse {
|
||||
Artcraft(ArtcraftSplatResponsePayload),
|
||||
WorldLabs(WorldLabsSplatResponsePayload),
|
||||
}
|
||||
|
||||
impl GenerateSplatResponse {
|
||||
pub fn get_artcraft_payload(&self) -> Option<ArtcraftSplatResponsePayload> {
|
||||
match self {
|
||||
Self::Artcraft(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_worldlabs_payload(&self) -> Option<WorldLabsSplatResponsePayload> {
|
||||
match self {
|
||||
Self::WorldLabs(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,18 @@
|
||||
// New audio-style pipeline (marble 1.x; use `GenerateSplatRequestBuilder::build2()`).
|
||||
pub mod generate_splat_request_builder;
|
||||
pub mod providers;
|
||||
pub mod splat_generation_draft;
|
||||
pub mod splat_generation_draft_context;
|
||||
pub mod splat_generation_draft_or_request;
|
||||
pub mod splat_generation_request;
|
||||
|
||||
// Shared between the new and legacy pipelines.
|
||||
pub mod generate_splat_response;
|
||||
pub mod splat_generation_cost_estimate;
|
||||
|
||||
// Legacy plan-based pipeline (marble 0.1 only; still used by the desktop app).
|
||||
pub mod cost;
|
||||
pub mod execute;
|
||||
pub mod generate_splat_request;
|
||||
pub mod generate_splat_response;
|
||||
pub mod plan;
|
||||
pub mod splat_generation_cost_estimate;
|
||||
pub mod splat_generation_plan;
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_splat_cost_and_generate_request::OmniGenSplatCostAndGenerateRequest;
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel as CommonSplatModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
|
||||
use crate::generate::generate_splat::providers::artcraft::resolve::{
|
||||
resolve_image_list_ref, resolve_video_ref,
|
||||
};
|
||||
|
||||
/// Build an `OmniGenSplatCostAndGenerateRequest` from the builder. The
|
||||
/// Artcraft provider is a thin forwarding shim: the storyteller-web omni
|
||||
/// splat endpoint owns per-model validation and option mapping, so the
|
||||
/// builder's options are passed through as-is.
|
||||
pub fn build_artcraft_omni_splat_request(
|
||||
mut builder: GenerateSplatRequestBuilder,
|
||||
model: CommonSplatModelEnum,
|
||||
) -> Result<OmniGenSplatCostAndGenerateRequest, ArtcraftRouterError> {
|
||||
let reference_image_media_tokens = resolve_image_list_ref(builder.reference_images.take())?;
|
||||
let reference_video_media_token = resolve_video_ref(builder.reference_video.take())?;
|
||||
let idempotency_token = builder.get_or_generate_idempotency_token();
|
||||
|
||||
Ok(OmniGenSplatCostAndGenerateRequest {
|
||||
idempotency_token: Some(idempotency_token),
|
||||
model: Some(model),
|
||||
prompt: builder.prompt.take(),
|
||||
reference_image_media_tokens,
|
||||
reference_video_media_token,
|
||||
is_panoramic: builder.is_panoramic,
|
||||
disable_recaption: builder.disable_recaption,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_splat_model::RouterSplatModel;
|
||||
use crate::api::video_ref::VideoRef;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_fields_are_forwarded() {
|
||||
let builder = GenerateSplatRequestBuilder {
|
||||
prompt: Some("a cozy cabin".to_string()),
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_image".to_string()),
|
||||
])),
|
||||
reference_video: Some(VideoRef::MediaFileToken(MediaFileToken::new("mf_video".to_string()))),
|
||||
is_panoramic: Some(true),
|
||||
disable_recaption: Some(true),
|
||||
idempotency_token: Some("test-token".to_string()),
|
||||
..base_builder()
|
||||
};
|
||||
|
||||
let request = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::Marble1p0)
|
||||
.expect("build");
|
||||
|
||||
assert_eq!(request.idempotency_token.as_deref(), Some("test-token"));
|
||||
assert_eq!(request.model, Some(CommonSplatModelEnum::Marble1p0));
|
||||
assert_eq!(request.prompt.as_deref(), Some("a cozy cabin"));
|
||||
assert_eq!(request.reference_image_media_tokens.as_ref().map(|t| t.len()), Some(1));
|
||||
assert_eq!(request.reference_video_media_token.as_ref().map(|t| t.as_str()), Some("mf_video"));
|
||||
assert_eq!(request.is_panoramic, Some(true));
|
||||
assert_eq!(request.disable_recaption, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idempotency_token_is_generated_when_missing() {
|
||||
let request = build_artcraft_omni_splat_request(base_builder(), CommonSplatModelEnum::Marble1p0Draft)
|
||||
.expect("build");
|
||||
assert!(request.idempotency_token.is_some());
|
||||
assert!(!request.idempotency_token.unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_url_references_are_rejected() {
|
||||
let builder = GenerateSplatRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec!["https://example.com/a.png".to_string()])),
|
||||
..base_builder()
|
||||
};
|
||||
let result = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::Marble1p0);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_url_references_are_rejected() {
|
||||
let builder = GenerateSplatRequestBuilder {
|
||||
reference_video: Some(VideoRef::Url("https://example.com/a.mp4".to_string())),
|
||||
..base_builder()
|
||||
};
|
||||
let result = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::Marble1p0);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
));
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
model: RouterSplatModel::Marble1p0,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("a cozy cabin".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_splat_cost_and_generate_request::OmniGenSplatCostAndGenerateRequest;
|
||||
use worldlabs_api_client::pricing::check_pricing::InputType;
|
||||
|
||||
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
|
||||
|
||||
/// Flat prices for a splat model by input type, in USD cents.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ArtcraftSplatPriceTable {
|
||||
pub image_panorama: u64,
|
||||
pub text: u64,
|
||||
pub image_non_panorama: u64,
|
||||
pub multi_image: u64,
|
||||
pub video: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftSplatPriceTable {
|
||||
pub(crate) fn cost_in_usd_cents(&self, input_type: InputType) -> u64 {
|
||||
match input_type {
|
||||
InputType::ImagePanorama => self.image_panorama,
|
||||
InputType::Text => self.text,
|
||||
InputType::ImageNonPanorama => self.image_non_panorama,
|
||||
InputType::MultiImage => self.multi_image,
|
||||
InputType::Video => self.video,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive the pricing input type from a forwarded omni splat request.
|
||||
/// Mirrors the request-assembly derivation: video wins over images, two or
|
||||
/// more images are multi-image, one image is panorama/non-panorama, and
|
||||
/// anything else prices as text.
|
||||
pub(crate) fn derive_input_type_for_pricing(
|
||||
request: &OmniGenSplatCostAndGenerateRequest,
|
||||
) -> InputType {
|
||||
let image_count = request.reference_image_media_tokens.as_ref()
|
||||
.map(|tokens| tokens.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
if request.reference_video_media_token.is_some() {
|
||||
InputType::Video
|
||||
} else if image_count >= 2 {
|
||||
InputType::MultiImage
|
||||
} else if image_count == 1 {
|
||||
if request.is_panoramic == Some(true) {
|
||||
InputType::ImagePanorama
|
||||
} else {
|
||||
InputType::ImageNonPanorama
|
||||
}
|
||||
} else {
|
||||
InputType::Text
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn artcraft_splat_cost_estimate(cost_in_usd_cents: u64) -> SplatGenerationCostEstimate {
|
||||
SplatGenerationCostEstimate {
|
||||
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,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel as CommonSplatModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
|
||||
use crate::generate::generate_splat::providers::artcraft::build_common::build_artcraft_omni_splat_request;
|
||||
use crate::generate::generate_splat::providers::artcraft::marble_1p0::request::ArtcraftMarble1p0RequestState;
|
||||
use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest;
|
||||
use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_marble_1p0(builder: GenerateSplatRequestBuilder) -> Result<SplatGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::Marble1p0)?;
|
||||
let state = ArtcraftMarble1p0RequestState { request };
|
||||
Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::ArtcraftMarble1p0(state)))
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
use worldlabs_api_client::pricing::check_pricing::InputType;
|
||||
|
||||
use crate::generate::generate_splat::providers::artcraft::cost_common::{
|
||||
artcraft_splat_cost_estimate, derive_input_type_for_pricing, ArtcraftSplatPriceTable,
|
||||
};
|
||||
use crate::generate::generate_splat::providers::artcraft::marble_1p0::request::ArtcraftMarble1p0RequestState;
|
||||
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
|
||||
|
||||
const PRICES: ArtcraftSplatPriceTable = ArtcraftSplatPriceTable {
|
||||
image_panorama: 156,
|
||||
text: 165,
|
||||
image_non_panorama: 165,
|
||||
multi_image: 167,
|
||||
video: 167,
|
||||
};
|
||||
|
||||
/// Marble 1.0 via Artcraft is flat priced by input type.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftMarble1p0CostState {
|
||||
pub input_type: InputType,
|
||||
}
|
||||
|
||||
impl ArtcraftMarble1p0CostState {
|
||||
pub fn from_request(request: &ArtcraftMarble1p0RequestState) -> Self {
|
||||
Self { input_type: derive_input_type_for_pricing(&request.request) }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> SplatGenerationCostEstimate {
|
||||
artcraft_splat_cost_estimate(PRICES.cost_in_usd_cents(self.input_type))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_splat_model::RouterSplatModel;
|
||||
use crate::api::video_ref::VideoRef;
|
||||
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
|
||||
|
||||
#[test]
|
||||
fn text_is_165_cents() {
|
||||
assert_eq!(estimate_usd_cents(text_builder()), 165);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_image_is_165_cents() {
|
||||
assert_eq!(estimate_usd_cents(single_image_builder(false)), 165);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panoramic_image_is_156_cents() {
|
||||
assert_eq!(estimate_usd_cents(single_image_builder(true)), 156);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_image_is_167_cents() {
|
||||
assert_eq!(estimate_usd_cents(multi_image_builder()), 167);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_is_167_cents() {
|
||||
assert_eq!(estimate_usd_cents(video_builder()), 167);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
model: RouterSplatModel::Marble1p0,
|
||||
provider: RouterProvider::Artcraft,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn text_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
prompt: Some("a cozy cabin in the snowy mountains".to_string()),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn single_image_builder(is_panoramic: bool) -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_image_1".to_string()),
|
||||
])),
|
||||
is_panoramic: Some(is_panoramic),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn multi_image_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_image_1".to_string()),
|
||||
MediaFileToken::new("mf_image_2".to_string()),
|
||||
])),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn video_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
reference_video: Some(VideoRef::MediaFileToken(MediaFileToken::new("mf_video".to_string()))),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateSplatRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_splat_cost_and_generate_request::OmniGenSplatCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse;
|
||||
use crate::generate::generate_splat::providers::artcraft::request_common::send_artcraft_omni_splat_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftMarble1p0RequestState {
|
||||
pub request: OmniGenSplatCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftMarble1p0RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_splat_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel as CommonSplatModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
|
||||
use crate::generate::generate_splat::providers::artcraft::build_common::build_artcraft_omni_splat_request;
|
||||
use crate::generate::generate_splat::providers::artcraft::marble_1p0_draft::request::ArtcraftMarble1p0DraftRequestState;
|
||||
use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest;
|
||||
use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_marble_1p0_draft(builder: GenerateSplatRequestBuilder) -> Result<SplatGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::Marble1p0Draft)?;
|
||||
let state = ArtcraftMarble1p0DraftRequestState { request };
|
||||
Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::ArtcraftMarble1p0Draft(state)))
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
use worldlabs_api_client::pricing::check_pricing::InputType;
|
||||
|
||||
use crate::generate::generate_splat::providers::artcraft::cost_common::{
|
||||
artcraft_splat_cost_estimate, derive_input_type_for_pricing, ArtcraftSplatPriceTable,
|
||||
};
|
||||
use crate::generate::generate_splat::providers::artcraft::marble_1p0_draft::request::ArtcraftMarble1p0DraftRequestState;
|
||||
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
|
||||
|
||||
const PRICES: ArtcraftSplatPriceTable = ArtcraftSplatPriceTable {
|
||||
image_panorama: 16,
|
||||
text: 24,
|
||||
image_non_panorama: 24,
|
||||
multi_image: 26,
|
||||
video: 26,
|
||||
};
|
||||
|
||||
/// Marble 1.0 Draft via Artcraft is flat priced by input type.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftMarble1p0DraftCostState {
|
||||
pub input_type: InputType,
|
||||
}
|
||||
|
||||
impl ArtcraftMarble1p0DraftCostState {
|
||||
pub fn from_request(request: &ArtcraftMarble1p0DraftRequestState) -> Self {
|
||||
Self { input_type: derive_input_type_for_pricing(&request.request) }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> SplatGenerationCostEstimate {
|
||||
artcraft_splat_cost_estimate(PRICES.cost_in_usd_cents(self.input_type))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_splat_model::RouterSplatModel;
|
||||
use crate::api::video_ref::VideoRef;
|
||||
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
|
||||
|
||||
#[test]
|
||||
fn text_is_24_cents() {
|
||||
assert_eq!(estimate_usd_cents(text_builder()), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_image_is_24_cents() {
|
||||
assert_eq!(estimate_usd_cents(single_image_builder(false)), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panoramic_image_is_16_cents() {
|
||||
assert_eq!(estimate_usd_cents(single_image_builder(true)), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_image_is_26_cents() {
|
||||
assert_eq!(estimate_usd_cents(multi_image_builder()), 26);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_is_26_cents() {
|
||||
assert_eq!(estimate_usd_cents(video_builder()), 26);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
model: RouterSplatModel::Marble1p0Draft,
|
||||
provider: RouterProvider::Artcraft,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn text_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
prompt: Some("a cozy cabin in the snowy mountains".to_string()),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn single_image_builder(is_panoramic: bool) -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_image_1".to_string()),
|
||||
])),
|
||||
is_panoramic: Some(is_panoramic),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn multi_image_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_image_1".to_string()),
|
||||
MediaFileToken::new("mf_image_2".to_string()),
|
||||
])),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn video_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
reference_video: Some(VideoRef::MediaFileToken(MediaFileToken::new("mf_video".to_string()))),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateSplatRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_splat_cost_and_generate_request::OmniGenSplatCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse;
|
||||
use crate::generate::generate_splat::providers::artcraft::request_common::send_artcraft_omni_splat_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftMarble1p0DraftRequestState {
|
||||
pub request: OmniGenSplatCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftMarble1p0DraftRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_splat_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_splat_model::RouterSplatModel;
|
||||
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
|
||||
use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse;
|
||||
use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest;
|
||||
use crate::test_helpers::get_artcraft_client;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn text_to_world() {
|
||||
let builder = GenerateSplatRequestBuilder {
|
||||
model: RouterSplatModel::Marble1p0Draft,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("A cozy cabin in the snowy mountains".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let client = get_artcraft_client();
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
SplatGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateSplatResponse::Artcraft(p) => {
|
||||
println!("inference_job_token={:?}", p.inference_job_token);
|
||||
println!("all_inference_job_tokens={:?}", p.all_inference_job_tokens);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
}
|
||||
|
||||
assert!(matches!(response, GenerateSplatResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel as CommonSplatModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
|
||||
use crate::generate::generate_splat::providers::artcraft::build_common::build_artcraft_omni_splat_request;
|
||||
use crate::generate::generate_splat::providers::artcraft::marble_1p1::request::ArtcraftMarble1p1RequestState;
|
||||
use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest;
|
||||
use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_marble_1p1(builder: GenerateSplatRequestBuilder) -> Result<SplatGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::Marble1p1)?;
|
||||
let state = ArtcraftMarble1p1RequestState { request };
|
||||
Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::ArtcraftMarble1p1(state)))
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
use worldlabs_api_client::pricing::check_pricing::InputType;
|
||||
|
||||
use crate::generate::generate_splat::providers::artcraft::cost_common::{
|
||||
artcraft_splat_cost_estimate, derive_input_type_for_pricing, ArtcraftSplatPriceTable,
|
||||
};
|
||||
use crate::generate::generate_splat::providers::artcraft::marble_1p1::request::ArtcraftMarble1p1RequestState;
|
||||
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
|
||||
|
||||
const PRICES: ArtcraftSplatPriceTable = ArtcraftSplatPriceTable {
|
||||
image_panorama: 156,
|
||||
text: 165,
|
||||
image_non_panorama: 165,
|
||||
multi_image: 167,
|
||||
video: 167,
|
||||
};
|
||||
|
||||
/// Marble 1.1 via Artcraft is flat priced by input type.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftMarble1p1CostState {
|
||||
pub input_type: InputType,
|
||||
}
|
||||
|
||||
impl ArtcraftMarble1p1CostState {
|
||||
pub fn from_request(request: &ArtcraftMarble1p1RequestState) -> Self {
|
||||
Self { input_type: derive_input_type_for_pricing(&request.request) }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> SplatGenerationCostEstimate {
|
||||
artcraft_splat_cost_estimate(PRICES.cost_in_usd_cents(self.input_type))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_splat_model::RouterSplatModel;
|
||||
use crate::api::video_ref::VideoRef;
|
||||
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
|
||||
|
||||
#[test]
|
||||
fn text_is_165_cents() {
|
||||
assert_eq!(estimate_usd_cents(text_builder()), 165);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_image_is_165_cents() {
|
||||
assert_eq!(estimate_usd_cents(single_image_builder(false)), 165);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panoramic_image_is_156_cents() {
|
||||
assert_eq!(estimate_usd_cents(single_image_builder(true)), 156);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_image_is_167_cents() {
|
||||
assert_eq!(estimate_usd_cents(multi_image_builder()), 167);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_is_167_cents() {
|
||||
assert_eq!(estimate_usd_cents(video_builder()), 167);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
model: RouterSplatModel::Marble1p1,
|
||||
provider: RouterProvider::Artcraft,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn text_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
prompt: Some("a cozy cabin in the snowy mountains".to_string()),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn single_image_builder(is_panoramic: bool) -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_image_1".to_string()),
|
||||
])),
|
||||
is_panoramic: Some(is_panoramic),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn multi_image_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_image_1".to_string()),
|
||||
MediaFileToken::new("mf_image_2".to_string()),
|
||||
])),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn video_builder() -> GenerateSplatRequestBuilder {
|
||||
GenerateSplatRequestBuilder {
|
||||
reference_video: Some(VideoRef::MediaFileToken(MediaFileToken::new("mf_video".to_string()))),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateSplatRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_splat_cost_and_generate_request::OmniGenSplatCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse;
|
||||
use crate::generate::generate_splat::providers::artcraft::request_common::send_artcraft_omni_splat_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftMarble1p1RequestState {
|
||||
pub request: OmniGenSplatCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftMarble1p1RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_splat_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel as CommonSplatModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder;
|
||||
use crate::generate::generate_splat::providers::artcraft::build_common::build_artcraft_omni_splat_request;
|
||||
use crate::generate::generate_splat::providers::artcraft::marble_1p1_plus::request::ArtcraftMarble1p1PlusRequestState;
|
||||
use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest;
|
||||
use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_marble_1p1_plus(builder: GenerateSplatRequestBuilder) -> Result<SplatGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::Marble1p1Plus)?;
|
||||
let state = ArtcraftMarble1p1PlusRequestState { request };
|
||||
Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::ArtcraftMarble1p1Plus(state)))
|
||||
}
|
||||
Loaded 100 of 212 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user