Compare commits

..
Author SHA1 Message Date
bflatastic 5a58a60475 Add Audio to webapp home cards; align ToggleButton height to 34px
- Home: new Audio card after Video linking to /create-audio (pink accent, faMusic)
- ToggleButton: h-9 (36px) -> h-[34px]/w-[34px] to match GenerateButton and popover trigger pills in promptbox toolbars
2026-07-07 23:38:47 +07:00
bflatastic 55fab85170 Add Create Audio pages (Suno + Seed Audio) with waveform gallery playback to desktop and webapp 2026-07-07 20:50:08 +07:00
404 changed files with 186 additions and 26267 deletions
-17
View File
@@ -77,20 +77,3 @@ Organize for top-to-bottom reading. Important things first, details later.
- Private helpers go *below* the methods that call them
- Among helpers: meatier logic above leaf-level formatters
- **In test modules**: constants first, then test cases (grouped into sub-modules when 2+), then helper functions last
## Markdown
- **Tables must be space-padded so columns align in plain text.** Markdown
tables are read raw (terminals, diffs, editors) at least as often as
rendered, and condensed tables are unreadable there. Pad every cell to its
column width:
```markdown
| Model | Configuration | Credits | Speed | Score |
|--------------|---------------|--------------------|---------|
| Meshy 6 | text or image | 104 | 80.0 | +24 |
| Rodin 2.5 | text or image | 13 | 10.0 | +3 |
```
Not: `| Model | Configuration | Credits | Speed | Score |` packed tight
with varying widths per row.
@@ -1,5 +1,3 @@
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;
@@ -1,73 +0,0 @@
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_mesh_quality::CommonMeshQuality;
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>,
/// Input mesh file for mesh-to-mesh models (part splitting, retopology).
/// Required by `hunyuan_3d_3p1_part` (FBX) and
/// `hunyuan_3d_3p1_topology` (GLB/OBJ).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_mesh_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>,
/// Whether to generate textures. Some models (Tripo3D) can skip texturing
/// for a cheaper, faster generation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_texture: Option<bool>,
/// Texture quality. `Detailed` costs extra on some models (Tripo3D's HD
/// texture tier).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub texture_quality: Option<CommonMeshQuality>,
/// Geometry quality. `Detailed` costs extra on some models (Tripo3D).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub geometry_quality: Option<CommonMeshQuality>,
}
@@ -1,34 +0,0 @@
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,5 +1,3 @@
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;
@@ -1,32 +0,0 @@
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>,
}
@@ -1,32 +0,0 @@
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>,
}
@@ -2,7 +2,7 @@ use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
/// Response body for the omni-gen video cost estimate endpoint.
#[derive(Debug, Serialize, Deserialize, ToSchema)]
#[derive(Serialize, Deserialize, ToSchema)]
pub struct OmniGenVideoCostResponse {
pub success: bool,
@@ -1,5 +1,3 @@
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;
@@ -1,16 +0,0 @@
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>,
}
@@ -1,16 +0,0 @@
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,5 +1,3 @@
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;
@@ -1,134 +0,0 @@
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>,
/// Whether the model takes an existing mesh file as input
/// (mesh-to-mesh models like part splitting and retopology).
#[serde(skip_serializing_if = "Option::is_none")]
pub mesh_input_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>,
/// Whether texture generation can be toggled off (untextured output).
#[serde(skip_serializing_if = "Option::is_none")]
pub texture_toggle_supported: Option<bool>,
/// Whether a texture quality level (standard/detailed) can be selected.
#[serde(skip_serializing_if = "Option::is_none")]
pub texture_quality_supported: Option<bool>,
/// Whether a geometry quality level (standard/detailed) can be selected.
#[serde(skip_serializing_if = "Option::is_none")]
pub geometry_quality_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_input_supported: None,
mesh_output_types: None,
polygon_types: None,
face_count_supported: None,
pbr_supported: None,
texture_toggle_supported: None,
texture_quality_supported: None,
geometry_quality_supported: None,
is_disabled: None,
}
}
}
@@ -1,98 +0,0 @@
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 @@
pub mod omni_gen_mesh;
@@ -1,21 +0,0 @@
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,5 +1,3 @@
pub mod audio;
pub mod image;
pub mod mesh;
pub mod splat;
pub mod video;
@@ -1 +0,0 @@
pub mod omni_gen_splat;
@@ -1,21 +0,0 @@
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?)
}
@@ -1,9 +0,0 @@
use tokens::tokens::media_files::MediaFileToken;
/// Reference to an input 3D mesh file (e.g. GLB/OBJ/FBX) for mesh-to-mesh
/// models like part splitting and retopology.
#[derive(Clone, Debug)]
pub enum MeshRef {
MediaFileToken(MediaFileToken),
Url(String),
}
@@ -2,16 +2,13 @@ pub mod audio_list_ref;
pub mod character_list_ref;
pub mod image_list_ref;
pub mod image_ref;
pub mod mesh_ref;
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;
@@ -1,107 +0,0 @@
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,
/// Hunyuan 3D v3.1 Pro: text or (multi-view) image input.
#[serde(rename = "hunyuan_3d_3p1_pro")]
Hunyuan3d3p1Pro,
/// Hunyuan 3D v3.1 Rapid: the fast, low-cost tier. Text or single-image
/// input.
#[serde(rename = "hunyuan_3d_3p1_rapid")]
Hunyuan3d3p1Rapid,
/// Hunyuan 3D v3.1 Part: splits an existing mesh into semantic parts.
/// Takes a mesh file as input, not text/images.
#[serde(rename = "hunyuan_3d_3p1_part")]
Hunyuan3d3p1Part,
/// Hunyuan 3D v3.1 Smart Topology: retopologizes an existing mesh.
/// Takes a mesh file as input, not text/images.
#[serde(rename = "hunyuan_3d_3p1_topology")]
Hunyuan3d3p1SmartTopology,
/// Tripo3D H3.1: text, single-image, or multi-view image input.
#[serde(rename = "tripo3d_h3p1")]
Tripo3dH3p1,
/// Meshy 6: text or single-image input.
#[serde(rename = "meshy_v6")]
MeshyV6,
/// Hyper3D Rodin v2.5 Fast: text or image(s) input.
#[serde(rename = "rodin_2p5_fast")]
Rodin2p5Fast,
}
#[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");
assert_serde_round_trip(RouterMeshModel::Hunyuan3d3p1Pro, "hunyuan_3d_3p1_pro");
assert_serde_round_trip(RouterMeshModel::Hunyuan3d3p1Rapid, "hunyuan_3d_3p1_rapid");
assert_serde_round_trip(RouterMeshModel::Hunyuan3d3p1Part, "hunyuan_3d_3p1_part");
assert_serde_round_trip(RouterMeshModel::Hunyuan3d3p1SmartTopology, "hunyuan_3d_3p1_topology");
assert_serde_round_trip(RouterMeshModel::Tripo3dH3p1, "tripo3d_h3p1");
assert_serde_round_trip(RouterMeshModel::MeshyV6, "meshy_v6");
assert_serde_round_trip(RouterMeshModel::Rodin2p5Fast, "rodin_2p5_fast");
}
#[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),
(RouterMeshModel::Hunyuan3d3p1Pro, CommonMeshModel::Hunyuan3d3p1Pro),
(RouterMeshModel::Hunyuan3d3p1Rapid, CommonMeshModel::Hunyuan3d3p1Rapid),
(RouterMeshModel::Hunyuan3d3p1Part, CommonMeshModel::Hunyuan3d3p1Part),
(RouterMeshModel::Hunyuan3d3p1SmartTopology, CommonMeshModel::Hunyuan3d3p1SmartTopology),
(RouterMeshModel::Tripo3dH3p1, CommonMeshModel::Tripo3dH3p1),
(RouterMeshModel::MeshyV6, CommonMeshModel::MeshyV6),
(RouterMeshModel::Rodin2p5Fast, CommonMeshModel::Rodin2p5Fast),
];
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,5 +9,4 @@ pub enum RouterProvider {
GmiCloud,
GrokApi,
Seedance2Pro,
WorldLabs,
}
@@ -5,68 +5,9 @@ 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);
}
}
@@ -1,7 +0,0 @@
use tokens::tokens::media_files::MediaFileToken;
#[derive(Clone, Debug)]
pub enum VideoRef {
MediaFileToken(MediaFileToken),
Url(String),
}
@@ -8,4 +8,3 @@ 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,7 +3,6 @@ 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 {
@@ -12,7 +11,6 @@ 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 {
@@ -40,9 +38,4 @@ 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,7 +4,6 @@ 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>,
@@ -12,7 +11,6 @@ pub struct MultiRouterClientBuilder {
gmicloud_client: Option<RouterGmiCloudClient>,
grok_api_client: Option<RouterGrokApiClient>,
seedance2pro_client: Option<RouterSeedance2ProClient>,
worldlabs_client: Option<RouterWorldLabsClient>,
}
impl MultiRouterClientBuilder {
@@ -23,7 +21,6 @@ impl MultiRouterClientBuilder {
gmicloud_client: None,
grok_api_client: None,
seedance2pro_client: None,
worldlabs_client: None,
}
}
@@ -52,11 +49,6 @@ 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,
@@ -64,7 +56,6 @@ 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,7 +4,6 @@ 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 {
@@ -14,7 +13,6 @@ pub enum RouterClient {
GmiCloud(RouterGmiCloudClient),
GrokApi(RouterGrokApiClient),
Seedance2Pro(RouterSeedance2ProClient),
WorldLabs(RouterWorldLabsClient),
}
impl RouterClient {
@@ -57,12 +55,4 @@ 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)),
}
}
}
@@ -1,16 +0,0 @@
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,7 +10,6 @@ pub enum ClientType {
GmiCloud,
GrokApi,
Seedance2Pro,
WorldLabs,
}
impl Display for ClientType {
@@ -21,7 +20,6 @@ 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,7 +5,6 @@ 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 {
@@ -14,7 +13,6 @@ pub enum ProviderError {
GmiCloud(GmiCloudError),
Grok(GrokError),
Seedance2Pro(Seedance2ProError),
WorldLabs(WorldLabsError),
}
impl Error for ProviderError {}
@@ -27,7 +25,6 @@ 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),
}
}
}
@@ -61,9 +58,3 @@ impl From<Seedance2ProError> for ProviderError {
Self::Seedance2Pro(error)
}
}
impl From<WorldLabsError> for ProviderError {
fn from(error: WorldLabsError) -> Self {
Self::WorldLabs(error)
}
}
@@ -1,313 +0,0 @@
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
use enums::common::generation::common_mesh_quality::CommonMeshQuality;
use enums::common::generation::common_polygon_type::CommonPolygonType;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::mesh_ref::MeshRef;
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::artcraft::hunyuan_3d_3p1_part::build::build_artcraft_hunyuan_3d_3p1_part;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_pro::build::build_artcraft_hunyuan_3d_3p1_pro;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_rapid::build::build_artcraft_hunyuan_3d_3p1_rapid;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_smart_topology::build::build_artcraft_hunyuan_3d_3p1_smart_topology;
use crate::generate::generate_mesh::providers::artcraft::meshy_v6::build::build_artcraft_meshy_v6;
use crate::generate::generate_mesh::providers::artcraft::rodin_2p5_fast::build::build_artcraft_rodin_2p5_fast;
use crate::generate::generate_mesh::providers::artcraft::tripo3d_h3p1::build::build_artcraft_tripo3d_h3p1;
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;
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_3p1_part::build::build_fal_hunyuan_3d_3p1_part;
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_3p1_pro::build::build_fal_hunyuan_3d_3p1_pro;
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_3p1_rapid::build::build_fal_hunyuan_3d_3p1_rapid;
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_3p1_smart_topology::build::build_fal_hunyuan_3d_3p1_smart_topology;
use crate::generate::generate_mesh::providers::fal::meshy_v6::build::build_fal_meshy_v6;
use crate::generate::generate_mesh::providers::fal::rodin_2p5_fast::build::build_fal_rodin_2p5_fast;
use crate::generate::generate_mesh::providers::fal::tripo3d_h3p1::build::build_fal_tripo3d_h3p1;
/// 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>,
/// Input mesh file for mesh-to-mesh models (part splitting, retopology).
pub input_mesh: Option<MeshRef>,
/// 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>,
/// Whether to generate textures. Some models (Tripo3D) can skip texturing
/// for a cheaper, faster generation.
pub enable_texture: Option<bool>,
/// Texture quality. `Detailed` may cost extra (e.g. Tripo3D's HD tier).
pub texture_quality: Option<CommonMeshQuality>,
/// Geometry quality. `Detailed` may cost extra (e.g. Tripo3D).
pub geometry_quality: Option<CommonMeshQuality>,
/// 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,
input_mesh: None,
mesh_output_type: None,
polygon_type: None,
face_count: None,
enable_pbr: None,
enable_texture: None,
texture_quality: None,
geometry_quality: 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),
(RouterProvider::Artcraft, RouterMeshModel::Hunyuan3d3p1Pro) => build_artcraft_hunyuan_3d_3p1_pro(self),
(RouterProvider::Artcraft, RouterMeshModel::Hunyuan3d3p1Rapid) => build_artcraft_hunyuan_3d_3p1_rapid(self),
(RouterProvider::Artcraft, RouterMeshModel::Hunyuan3d3p1Part) => build_artcraft_hunyuan_3d_3p1_part(self),
(RouterProvider::Artcraft, RouterMeshModel::Hunyuan3d3p1SmartTopology) => build_artcraft_hunyuan_3d_3p1_smart_topology(self),
(RouterProvider::Artcraft, RouterMeshModel::Tripo3dH3p1) => build_artcraft_tripo3d_h3p1(self),
(RouterProvider::Artcraft, RouterMeshModel::MeshyV6) => build_artcraft_meshy_v6(self),
(RouterProvider::Artcraft, RouterMeshModel::Rodin2p5Fast) => build_artcraft_rodin_2p5_fast(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),
(RouterProvider::Fal, RouterMeshModel::Hunyuan3d3p1Pro) => build_fal_hunyuan_3d_3p1_pro(self),
(RouterProvider::Fal, RouterMeshModel::Hunyuan3d3p1Rapid) => build_fal_hunyuan_3d_3p1_rapid(self),
(RouterProvider::Fal, RouterMeshModel::Hunyuan3d3p1Part) => build_fal_hunyuan_3d_3p1_part(self),
(RouterProvider::Fal, RouterMeshModel::Hunyuan3d3p1SmartTopology) => build_fal_hunyuan_3d_3p1_smart_topology(self),
(RouterProvider::Fal, RouterMeshModel::Tripo3dH3p1) => build_fal_tripo3d_h3p1(self),
(RouterProvider::Fal, RouterMeshModel::MeshyV6) => build_fal_meshy_v6(self),
(RouterProvider::Fal, RouterMeshModel::Rodin2p5Fast) => build_fal_rodin_2p5_fast(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:?}"),
}
}
}
@@ -1,51 +0,0 @@
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,
}
}
}
@@ -1,16 +0,0 @@
/// 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>,
}
@@ -1,35 +0,0 @@
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 {}
}
}
@@ -1,25 +0,0 @@
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()
}
}
@@ -1,28 +0,0 @@
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(),
}
}
}
@@ -1,214 +0,0 @@
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::artcraft::hunyuan_3d_3p1_part::cost::ArtcraftHunyuan3d3p1PartCostState;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_part::request::ArtcraftHunyuan3d3p1PartRequestState;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_pro::cost::ArtcraftHunyuan3d3p1ProCostState;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_pro::request::ArtcraftHunyuan3d3p1ProRequestState;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_rapid::cost::ArtcraftHunyuan3d3p1RapidCostState;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_rapid::request::ArtcraftHunyuan3d3p1RapidRequestState;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_smart_topology::cost::ArtcraftHunyuan3d3p1SmartTopologyCostState;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_smart_topology::request::ArtcraftHunyuan3d3p1SmartTopologyRequestState;
use crate::generate::generate_mesh::providers::artcraft::meshy_v6::cost::ArtcraftMeshyV6CostState;
use crate::generate::generate_mesh::providers::artcraft::meshy_v6::request::ArtcraftMeshyV6RequestState;
use crate::generate::generate_mesh::providers::artcraft::rodin_2p5_fast::cost::ArtcraftRodin2p5FastCostState;
use crate::generate::generate_mesh::providers::artcraft::rodin_2p5_fast::request::ArtcraftRodin2p5FastRequestState;
use crate::generate::generate_mesh::providers::artcraft::tripo3d_h3p1::cost::ArtcraftTripo3dH3p1CostState;
use crate::generate::generate_mesh::providers::artcraft::tripo3d_h3p1::request::ArtcraftTripo3dH3p1RequestState;
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;
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_3p1_part::cost::FalHunyuan3d3p1PartCostState;
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_3p1_part::request::FalHunyuan3d3p1PartRequestState;
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_3p1_pro::cost::{
FalHunyuan3d3p1ProImageCostState, FalHunyuan3d3p1ProTextCostState,
};
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_3p1_pro::request::{
FalHunyuan3d3p1ProImageRequestState, FalHunyuan3d3p1ProTextRequestState,
};
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_3p1_rapid::cost::{
FalHunyuan3d3p1RapidImageCostState, FalHunyuan3d3p1RapidTextCostState,
};
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_3p1_rapid::request::{
FalHunyuan3d3p1RapidImageRequestState, FalHunyuan3d3p1RapidTextRequestState,
};
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_3p1_smart_topology::cost::FalHunyuan3d3p1SmartTopologyCostState;
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_3p1_smart_topology::request::FalHunyuan3d3p1SmartTopologyRequestState;
use crate::generate::generate_mesh::providers::fal::meshy_v6::cost::{
FalMeshyV6ImageCostState, FalMeshyV6TextCostState,
};
use crate::generate::generate_mesh::providers::fal::meshy_v6::request::{
FalMeshyV6ImageRequestState, FalMeshyV6TextRequestState,
};
use crate::generate::generate_mesh::providers::fal::rodin_2p5_fast::cost::{
FalRodin2p5FastImageCostState, FalRodin2p5FastTextCostState,
};
use crate::generate::generate_mesh::providers::fal::rodin_2p5_fast::request::{
FalRodin2p5FastImageRequestState, FalRodin2p5FastTextRequestState,
};
use crate::generate::generate_mesh::providers::fal::tripo3d_h3p1::cost::{
FalTripo3dH3p1ImageCostState, FalTripo3dH3p1MultiviewCostState, FalTripo3dH3p1TextCostState,
};
use crate::generate::generate_mesh::providers::fal::tripo3d_h3p1::request::{
FalTripo3dH3p1ImageRequestState, FalTripo3dH3p1MultiviewRequestState, FalTripo3dH3p1TextRequestState,
};
#[derive(Clone, Debug)]
pub enum MeshGenerationRequest {
ArtcraftHunyuan3d2p0(ArtcraftHunyuan3d2p0RequestState),
ArtcraftHunyuan3d2p1(ArtcraftHunyuan3d2p1RequestState),
ArtcraftHunyuan3d3(ArtcraftHunyuan3d3RequestState),
ArtcraftHunyuan3d3Sketch(ArtcraftHunyuan3d3SketchRequestState),
ArtcraftHunyuan3d3p1Pro(ArtcraftHunyuan3d3p1ProRequestState),
ArtcraftHunyuan3d3p1Rapid(ArtcraftHunyuan3d3p1RapidRequestState),
ArtcraftHunyuan3d3p1Part(ArtcraftHunyuan3d3p1PartRequestState),
ArtcraftHunyuan3d3p1SmartTopology(ArtcraftHunyuan3d3p1SmartTopologyRequestState),
ArtcraftTripo3dH3p1(ArtcraftTripo3dH3p1RequestState),
ArtcraftMeshyV6(ArtcraftMeshyV6RequestState),
ArtcraftRodin2p5Fast(ArtcraftRodin2p5FastRequestState),
FalHunyuan3d2p0(FalHunyuan3d2p0RequestState),
FalHunyuan3d2p1(FalHunyuan3d2p1RequestState),
FalHunyuan3d3Image(FalHunyuan3d3ImageRequestState),
FalHunyuan3d3Text(FalHunyuan3d3TextRequestState),
FalHunyuan3d3Sketch(FalHunyuan3d3SketchRequestState),
FalHunyuan3d3p1ProImage(FalHunyuan3d3p1ProImageRequestState),
FalHunyuan3d3p1ProText(FalHunyuan3d3p1ProTextRequestState),
FalHunyuan3d3p1RapidImage(FalHunyuan3d3p1RapidImageRequestState),
FalHunyuan3d3p1RapidText(FalHunyuan3d3p1RapidTextRequestState),
FalHunyuan3d3p1Part(FalHunyuan3d3p1PartRequestState),
FalHunyuan3d3p1SmartTopology(FalHunyuan3d3p1SmartTopologyRequestState),
FalTripo3dH3p1Text(FalTripo3dH3p1TextRequestState),
FalTripo3dH3p1Image(FalTripo3dH3p1ImageRequestState),
FalTripo3dH3p1Multiview(FalTripo3dH3p1MultiviewRequestState),
FalMeshyV6Text(FalMeshyV6TextRequestState),
FalMeshyV6Image(FalMeshyV6ImageRequestState),
FalRodin2p5FastText(FalRodin2p5FastTextRequestState),
FalRodin2p5FastImage(FalRodin2p5FastImageRequestState),
}
impl MeshGenerationRequest {
pub fn get_provider(&self) -> RouterProvider {
match self {
Self::ArtcraftHunyuan3d2p0(_)
| Self::ArtcraftHunyuan3d2p1(_)
| Self::ArtcraftHunyuan3d3(_)
| Self::ArtcraftHunyuan3d3Sketch(_)
| Self::ArtcraftHunyuan3d3p1Pro(_)
| Self::ArtcraftHunyuan3d3p1Rapid(_)
| Self::ArtcraftHunyuan3d3p1Part(_)
| Self::ArtcraftHunyuan3d3p1SmartTopology(_)
| Self::ArtcraftTripo3dH3p1(_)
| Self::ArtcraftMeshyV6(_)
| Self::ArtcraftRodin2p5Fast(_) => RouterProvider::Artcraft,
Self::FalHunyuan3d2p0(_)
| Self::FalHunyuan3d2p1(_)
| Self::FalHunyuan3d3Image(_)
| Self::FalHunyuan3d3Text(_)
| Self::FalHunyuan3d3Sketch(_)
| Self::FalHunyuan3d3p1ProImage(_)
| Self::FalHunyuan3d3p1ProText(_)
| Self::FalHunyuan3d3p1RapidImage(_)
| Self::FalHunyuan3d3p1RapidText(_)
| Self::FalHunyuan3d3p1Part(_)
| Self::FalHunyuan3d3p1SmartTopology(_)
| Self::FalTripo3dH3p1Text(_)
| Self::FalTripo3dH3p1Image(_)
| Self::FalTripo3dH3p1Multiview(_)
| Self::FalMeshyV6Text(_)
| Self::FalMeshyV6Image(_)
| Self::FalRodin2p5FastText(_)
| Self::FalRodin2p5FastImage(_) => RouterProvider::Fal,
}
}
/// Return a cost estimate to fulfill the request.
pub fn estimate_cost(&self) -> Result<MeshGenerationCostEstimate, ArtcraftRouterError> {
match self {
Self::ArtcraftHunyuan3d2p0(request) => Ok(ArtcraftHunyuan3d2p0CostState::from_request(request).estimate_cost()),
Self::ArtcraftHunyuan3d2p1(request) => Ok(ArtcraftHunyuan3d2p1CostState::from_request(request).estimate_cost()),
Self::ArtcraftHunyuan3d3(request) => Ok(ArtcraftHunyuan3d3CostState::from_request(request).estimate_cost()),
Self::ArtcraftHunyuan3d3Sketch(request) => Ok(ArtcraftHunyuan3d3SketchCostState::from_request(request).estimate_cost()),
Self::ArtcraftHunyuan3d3p1Pro(request) => Ok(ArtcraftHunyuan3d3p1ProCostState::from_request(request).estimate_cost()),
Self::ArtcraftHunyuan3d3p1Rapid(request) => Ok(ArtcraftHunyuan3d3p1RapidCostState::from_request(request).estimate_cost()),
Self::ArtcraftHunyuan3d3p1Part(request) => Ok(ArtcraftHunyuan3d3p1PartCostState::from_request(request).estimate_cost()),
Self::ArtcraftHunyuan3d3p1SmartTopology(request) => Ok(ArtcraftHunyuan3d3p1SmartTopologyCostState::from_request(request).estimate_cost()),
Self::ArtcraftTripo3dH3p1(request) => Ok(ArtcraftTripo3dH3p1CostState::from_request(request).estimate_cost()),
Self::ArtcraftMeshyV6(request) => Ok(ArtcraftMeshyV6CostState::from_request(request).estimate_cost()),
Self::ArtcraftRodin2p5Fast(request) => Ok(ArtcraftRodin2p5FastCostState::from_request(request).estimate_cost()),
Self::FalHunyuan3d2p0(request) => Ok(FalHunyuan3d2p0CostState::from_request(request).estimate_cost()),
Self::FalHunyuan3d2p1(request) => Ok(FalHunyuan3d2p1CostState::from_request(request).estimate_cost()),
Self::FalHunyuan3d3Image(request) => Ok(FalHunyuan3d3ImageCostState::from_request(request).estimate_cost()),
Self::FalHunyuan3d3Text(request) => Ok(FalHunyuan3d3TextCostState::from_request(request).estimate_cost()),
Self::FalHunyuan3d3Sketch(request) => Ok(FalHunyuan3d3SketchCostState::from_request(request).estimate_cost()),
Self::FalHunyuan3d3p1ProImage(request) => Ok(FalHunyuan3d3p1ProImageCostState::from_request(request).estimate_cost()),
Self::FalHunyuan3d3p1ProText(request) => Ok(FalHunyuan3d3p1ProTextCostState::from_request(request).estimate_cost()),
Self::FalHunyuan3d3p1RapidImage(request) => Ok(FalHunyuan3d3p1RapidImageCostState::from_request(request).estimate_cost()),
Self::FalHunyuan3d3p1RapidText(request) => Ok(FalHunyuan3d3p1RapidTextCostState::from_request(request).estimate_cost()),
Self::FalHunyuan3d3p1Part(request) => Ok(FalHunyuan3d3p1PartCostState::from_request(request).estimate_cost()),
Self::FalHunyuan3d3p1SmartTopology(request) => Ok(FalHunyuan3d3p1SmartTopologyCostState::from_request(request).estimate_cost()),
Self::FalTripo3dH3p1Text(request) => Ok(FalTripo3dH3p1TextCostState::from_request(request).estimate_cost()),
Self::FalTripo3dH3p1Image(request) => Ok(FalTripo3dH3p1ImageCostState::from_request(request).estimate_cost()),
Self::FalTripo3dH3p1Multiview(request) => Ok(FalTripo3dH3p1MultiviewCostState::from_request(request).estimate_cost()),
Self::FalMeshyV6Text(request) => Ok(FalMeshyV6TextCostState::from_request(request).estimate_cost()),
Self::FalMeshyV6Image(request) => Ok(FalMeshyV6ImageCostState::from_request(request).estimate_cost()),
Self::FalRodin2p5FastText(request) => Ok(FalRodin2p5FastTextCostState::from_request(request).estimate_cost()),
Self::FalRodin2p5FastImage(request) => Ok(FalRodin2p5FastImageCostState::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 {
Self::ArtcraftHunyuan3d2p0(request) => request.send(client.get_artcraft_client_ref()?).await,
Self::ArtcraftHunyuan3d2p1(request) => request.send(client.get_artcraft_client_ref()?).await,
Self::ArtcraftHunyuan3d3(request) => request.send(client.get_artcraft_client_ref()?).await,
Self::ArtcraftHunyuan3d3Sketch(request) => request.send(client.get_artcraft_client_ref()?).await,
Self::ArtcraftHunyuan3d3p1Pro(request) => request.send(client.get_artcraft_client_ref()?).await,
Self::ArtcraftHunyuan3d3p1Rapid(request) => request.send(client.get_artcraft_client_ref()?).await,
Self::ArtcraftHunyuan3d3p1Part(request) => request.send(client.get_artcraft_client_ref()?).await,
Self::ArtcraftHunyuan3d3p1SmartTopology(request) => request.send(client.get_artcraft_client_ref()?).await,
Self::ArtcraftTripo3dH3p1(request) => request.send(client.get_artcraft_client_ref()?).await,
Self::ArtcraftMeshyV6(request) => request.send(client.get_artcraft_client_ref()?).await,
Self::ArtcraftRodin2p5Fast(request) => request.send(client.get_artcraft_client_ref()?).await,
Self::FalHunyuan3d2p0(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalHunyuan3d2p1(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalHunyuan3d3Image(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalHunyuan3d3Text(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalHunyuan3d3Sketch(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalHunyuan3d3p1ProImage(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalHunyuan3d3p1ProText(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalHunyuan3d3p1RapidImage(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalHunyuan3d3p1RapidText(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalHunyuan3d3p1Part(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalHunyuan3d3p1SmartTopology(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalTripo3dH3p1Text(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalTripo3dH3p1Image(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalTripo3dH3p1Multiview(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalMeshyV6Text(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalMeshyV6Image(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalRodin2p5FastText(request) => request.send(client.get_fal_client_ref()?).await,
Self::FalRodin2p5FastImage(request) => request.send(client.get_fal_client_ref()?).await,
}
}
}
@@ -1,8 +0,0 @@
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;
@@ -1,140 +0,0 @@
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, resolve_mesh_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 input_mesh_media_token = resolve_mesh_ref(builder.input_mesh.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,
input_mesh_media_token,
mesh_output_type: builder.mesh_output_type,
polygon_type: builder.polygon_type,
face_count: builder.face_count,
enable_pbr: builder.enable_pbr,
enable_texture: builder.enable_texture,
texture_quality: builder.texture_quality,
geometry_quality: builder.geometry_quality,
})
}
#[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()
}
}
}
@@ -1,14 +0,0 @@
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)))
}
@@ -1,225 +0,0 @@
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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,64 +0,0 @@
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
}
}
@@ -1,14 +0,0 @@
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)))
}
@@ -1,115 +0,0 @@
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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,17 +0,0 @@
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
}
}
@@ -1,14 +0,0 @@
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)))
}
@@ -1,93 +0,0 @@
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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,17 +0,0 @@
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
}
}
@@ -1,14 +0,0 @@
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)))
}
@@ -1,93 +0,0 @@
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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,17 +0,0 @@
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
}
}
@@ -1,14 +0,0 @@
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_3p1_part::request::ArtcraftHunyuan3d3p1PartRequestState;
pub fn build_artcraft_hunyuan_3d_3p1_part(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d3p1Part)?;
let state = ArtcraftHunyuan3d3p1PartRequestState { request };
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftHunyuan3d3p1Part(state)))
}
@@ -1,78 +0,0 @@
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_part::request::ArtcraftHunyuan3d3p1PartRequestState;
const FLAT_COST_IN_USD_CENTS: u64 = 59;
/// Hunyuan 3D v3.1 Part via Artcraft: flat 59¢ per generation, with no
/// add-ons.
#[derive(Clone, Debug)]
pub struct ArtcraftHunyuan3d3p1PartCostState {
pub cost_in_usd_cents: u64,
}
impl ArtcraftHunyuan3d3p1PartCostState {
pub fn from_request(_request: &ArtcraftHunyuan3d3p1PartRequestState) -> Self {
Self { cost_in_usd_cents: FLAT_COST_IN_USD_CENTS }
}
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::mesh_ref::MeshRef;
use crate::api::router_mesh_model::RouterMeshModel;
use crate::api::router_provider::RouterProvider;
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
mod cost_table {
use super::*;
#[test]
fn cost_is_a_flat_fifty_nine_cents() {
assert_eq!(estimate_usd_cents(base_builder()), 59);
}
#[test]
fn options_do_not_change_the_price() {
let builder = GenerateMeshRequestBuilder {
enable_pbr: Some(true),
face_count: Some(100_000),
..base_builder()
};
assert_eq!(estimate_usd_cents(builder), 59);
}
}
// ── Helpers ──
fn base_builder() -> GenerateMeshRequestBuilder {
GenerateMeshRequestBuilder {
model: RouterMeshModel::Hunyuan3d3p1Part,
provider: RouterProvider::Artcraft,
input_mesh: Some(MeshRef::MediaFileToken(MediaFileToken::new("mf_mesh".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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,17 +0,0 @@
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 ArtcraftHunyuan3d3p1PartRequestState {
pub request: OmniGenMeshCostAndGenerateRequest,
}
impl ArtcraftHunyuan3d3p1PartRequestState {
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
send_artcraft_omni_mesh_request(&self.request, client).await
}
}
@@ -1,14 +0,0 @@
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_3p1_pro::request::ArtcraftHunyuan3d3p1ProRequestState;
pub fn build_artcraft_hunyuan_3d_3p1_pro(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d3p1Pro)?;
let state = ArtcraftHunyuan3d3p1ProRequestState { request };
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftHunyuan3d3p1Pro(state)))
}
@@ -1,183 +0,0 @@
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_pro::request::ArtcraftHunyuan3d3p1ProRequestState;
const BASE_COST_IN_USD_CENTS: u64 = 49;
const ADD_ON_COST_IN_USD_CENTS: u64 = 20;
/// Hunyuan 3D v3.1 Pro via Artcraft: flat 49¢ base price (any output type),
/// plus a flat 20¢ add-on for each extra option (PBR materials, custom face
/// count, and multi-view side images).
#[derive(Clone, Debug)]
pub struct ArtcraftHunyuan3d3p1ProCostState {
pub cost_in_usd_cents: u64,
}
impl ArtcraftHunyuan3d3p1ProCostState {
pub fn from_request(request: &ArtcraftHunyuan3d3p1ProRequestState) -> 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;
}
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_mesh_output_type::CommonMeshOutputType;
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 every_output_type_prices_the_same() {
let cases = [
Some(CommonMeshOutputType::Normal),
Some(CommonMeshOutputType::Geometry),
];
for output_type in cases {
let builder = GenerateMeshRequestBuilder {
mesh_output_type: output_type,
..base_builder()
};
assert_eq!(estimate_usd_cents(builder), 49, "for {output_type:?}");
}
}
#[test]
fn text_mode_prices_the_same_as_image_mode() {
// Text-only request (no image references).
let text_builder = GenerateMeshRequestBuilder {
model: RouterMeshModel::Hunyuan3d3p1Pro,
provider: RouterProvider::Artcraft,
prompt: Some("a red ceramic teapot".to_string()),
..Default::default()
};
assert_eq!(estimate_usd_cents(text_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 {
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), 49 + 20 + 20 + 20);
}
}
// ── Helpers ──
fn base_builder() -> GenerateMeshRequestBuilder {
GenerateMeshRequestBuilder {
model: RouterMeshModel::Hunyuan3d3p1Pro,
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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,64 +0,0 @@
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 ArtcraftHunyuan3d3p1ProRequestState {
pub request: OmniGenMeshCostAndGenerateRequest,
}
impl ArtcraftHunyuan3d3p1ProRequestState {
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::Hunyuan3d3p1Pro,
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
}
}
@@ -1,14 +0,0 @@
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_3p1_rapid::request::ArtcraftHunyuan3d3p1RapidRequestState;
pub fn build_artcraft_hunyuan_3d_3p1_rapid(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d3p1Rapid)?;
let state = ArtcraftHunyuan3d3p1RapidRequestState { request };
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftHunyuan3d3p1Rapid(state)))
}
@@ -1,124 +0,0 @@
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_rapid::request::ArtcraftHunyuan3d3p1RapidRequestState;
const BASE_COST_IN_USD_CENTS: u64 = 30;
const PBR_ADD_ON_COST_IN_USD_CENTS: u64 = 20;
/// Hunyuan 3D v3.1 Rapid via Artcraft: flat 30¢ base price, plus a flat 20¢
/// add-on for PBR materials.
#[derive(Clone, Debug)]
pub struct ArtcraftHunyuan3d3p1RapidCostState {
pub cost_in_usd_cents: u64,
}
impl ArtcraftHunyuan3d3p1RapidCostState {
pub fn from_request(request: &ArtcraftHunyuan3d3p1RapidRequestState) -> Self {
let request = &request.request;
let mut cost = BASE_COST_IN_USD_CENTS;
if request.enable_pbr.unwrap_or(false) {
cost += PBR_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_mesh_output_type::CommonMeshOutputType;
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::*;
mod base_costs {
use super::*;
#[test]
fn base_cost_is_thirty_cents() {
assert_eq!(estimate_usd_cents(base_builder()), 30);
}
#[test]
fn text_mode_prices_the_same_as_image_mode() {
// Text-only request (no image references).
let text_builder = GenerateMeshRequestBuilder {
model: RouterMeshModel::Hunyuan3d3p1Rapid,
provider: RouterProvider::Artcraft,
prompt: Some("a red ceramic teapot".to_string()),
..Default::default()
};
assert_eq!(estimate_usd_cents(text_builder), 30);
}
#[test]
fn output_type_does_not_change_the_price() {
let builder = GenerateMeshRequestBuilder {
mesh_output_type: Some(CommonMeshOutputType::Geometry),
..base_builder()
};
assert_eq!(estimate_usd_cents(builder), 30);
}
}
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), 30 + 20);
}
#[test]
fn pbr_false_adds_nothing() {
let builder = GenerateMeshRequestBuilder {
enable_pbr: Some(false),
..base_builder()
};
assert_eq!(estimate_usd_cents(builder), 30);
}
}
// ── Helpers ──
fn base_builder() -> GenerateMeshRequestBuilder {
GenerateMeshRequestBuilder {
model: RouterMeshModel::Hunyuan3d3p1Rapid,
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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,64 +0,0 @@
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 ArtcraftHunyuan3d3p1RapidRequestState {
pub request: OmniGenMeshCostAndGenerateRequest,
}
impl ArtcraftHunyuan3d3p1RapidRequestState {
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::Hunyuan3d3p1Rapid,
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
}
}
@@ -1,14 +0,0 @@
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_3p1_smart_topology::request::ArtcraftHunyuan3d3p1SmartTopologyRequestState;
pub fn build_artcraft_hunyuan_3d_3p1_smart_topology(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d3p1SmartTopology)?;
let state = ArtcraftHunyuan3d3p1SmartTopologyRequestState { request };
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftHunyuan3d3p1SmartTopology(state)))
}
@@ -1,78 +0,0 @@
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_3p1_smart_topology::request::ArtcraftHunyuan3d3p1SmartTopologyRequestState;
const FLAT_COST_IN_USD_CENTS: u64 = 98;
/// Hunyuan 3D v3.1 Smart Topology via Artcraft: flat 98¢ per generation,
/// with no add-ons.
#[derive(Clone, Debug)]
pub struct ArtcraftHunyuan3d3p1SmartTopologyCostState {
pub cost_in_usd_cents: u64,
}
impl ArtcraftHunyuan3d3p1SmartTopologyCostState {
pub fn from_request(_request: &ArtcraftHunyuan3d3p1SmartTopologyRequestState) -> Self {
Self { cost_in_usd_cents: FLAT_COST_IN_USD_CENTS }
}
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::mesh_ref::MeshRef;
use crate::api::router_mesh_model::RouterMeshModel;
use crate::api::router_provider::RouterProvider;
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
mod cost_table {
use super::*;
#[test]
fn cost_is_a_flat_ninety_eight_cents() {
assert_eq!(estimate_usd_cents(base_builder()), 98);
}
#[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), 98);
}
}
// ── Helpers ──
fn base_builder() -> GenerateMeshRequestBuilder {
GenerateMeshRequestBuilder {
model: RouterMeshModel::Hunyuan3d3p1SmartTopology,
provider: RouterProvider::Artcraft,
input_mesh: Some(MeshRef::MediaFileToken(MediaFileToken::new("mf_mesh".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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,17 +0,0 @@
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 ArtcraftHunyuan3d3p1SmartTopologyRequestState {
pub request: OmniGenMeshCostAndGenerateRequest,
}
impl ArtcraftHunyuan3d3p1SmartTopologyRequestState {
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
send_artcraft_omni_mesh_request(&self.request, client).await
}
}
@@ -1,14 +0,0 @@
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::meshy_v6::request::ArtcraftMeshyV6RequestState;
pub fn build_artcraft_meshy_v6(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::MeshyV6)?;
let state = ArtcraftMeshyV6RequestState { request };
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftMeshyV6(state)))
}
@@ -1,100 +0,0 @@
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
use crate::generate::generate_mesh::providers::artcraft::meshy_v6::request::ArtcraftMeshyV6RequestState;
const FLAT_COST_IN_USD_CENTS: u64 = 104;
/// Meshy 6 via Artcraft: flat price per generation, regardless of options
/// or input mode (text or image).
#[derive(Clone, Debug)]
pub struct ArtcraftMeshyV6CostState {
pub cost_in_usd_cents: u64,
}
impl ArtcraftMeshyV6CostState {
pub fn from_request(_request: &ArtcraftMeshyV6RequestState) -> Self {
Self { cost_in_usd_cents: FLAT_COST_IN_USD_CENTS }
}
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_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::router_mesh_model::RouterMeshModel;
use crate::api::router_provider::RouterProvider;
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
use super::*;
mod flat_pricing {
use super::*;
#[test]
fn image_mode_is_one_hundred_four_cents() {
assert_eq!(estimate_usd_cents(image_builder()), 104);
}
#[test]
fn text_mode_is_one_hundred_four_cents() {
assert_eq!(estimate_usd_cents(text_builder()), 104);
}
#[test]
fn options_do_not_change_the_price() {
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()
};
assert_eq!(estimate_usd_cents(builder), 104);
}
}
// ── Helpers ──
fn image_builder() -> GenerateMeshRequestBuilder {
GenerateMeshRequestBuilder {
model: RouterMeshModel::MeshyV6,
provider: RouterProvider::Artcraft,
reference_images: Some(ImageListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_front".to_string()),
])),
..Default::default()
}
}
fn text_builder() -> GenerateMeshRequestBuilder {
GenerateMeshRequestBuilder {
model: RouterMeshModel::MeshyV6,
provider: RouterProvider::Artcraft,
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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,64 +0,0 @@
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 ArtcraftMeshyV6RequestState {
pub request: OmniGenMeshCostAndGenerateRequest,
}
impl ArtcraftMeshyV6RequestState {
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::MeshyV6,
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
}
}
@@ -1,14 +0,0 @@
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;
pub mod hunyuan_3d_3p1_part;
pub mod hunyuan_3d_3p1_pro;
pub mod hunyuan_3d_3p1_rapid;
pub mod hunyuan_3d_3p1_smart_topology;
pub mod meshy_v6;
pub mod rodin_2p5_fast;
pub mod tripo3d_h3p1;
@@ -1,29 +0,0 @@
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,
}))
}
@@ -1,47 +0,0 @@
//! 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::api::mesh_ref::MeshRef;
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))
}
}
}
pub(super) fn resolve_mesh_ref(
mesh_ref: Option<MeshRef>,
) -> Result<Option<MediaFileToken>, ArtcraftRouterError> {
match mesh_ref {
None => Ok(None),
Some(MeshRef::MediaFileToken(token)) => Ok(Some(token)),
Some(MeshRef::Url(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
@@ -1,14 +0,0 @@
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::rodin_2p5_fast::request::ArtcraftRodin2p5FastRequestState;
pub fn build_artcraft_rodin_2p5_fast(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Rodin2p5Fast)?;
let state = ArtcraftRodin2p5FastRequestState { request };
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftRodin2p5Fast(state)))
}
@@ -1,97 +0,0 @@
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
use crate::generate::generate_mesh::providers::artcraft::rodin_2p5_fast::request::ArtcraftRodin2p5FastRequestState;
const FLAT_COST_IN_USD_CENTS: u64 = 13;
/// Hyper3D Rodin v2.5 Fast via Artcraft: flat price per generation,
/// regardless of options or input mode (text or image).
#[derive(Clone, Debug)]
pub struct ArtcraftRodin2p5FastCostState {
pub cost_in_usd_cents: u64,
}
impl ArtcraftRodin2p5FastCostState {
pub fn from_request(_request: &ArtcraftRodin2p5FastRequestState) -> Self {
Self { cost_in_usd_cents: FLAT_COST_IN_USD_CENTS }
}
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_mesh_output_type::CommonMeshOutputType;
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::*;
mod flat_pricing {
use super::*;
#[test]
fn image_mode_is_thirteen_cents() {
assert_eq!(estimate_usd_cents(image_builder()), 13);
}
#[test]
fn text_mode_is_thirteen_cents() {
assert_eq!(estimate_usd_cents(text_builder()), 13);
}
#[test]
fn options_do_not_change_the_price() {
let builder = GenerateMeshRequestBuilder {
mesh_output_type: Some(CommonMeshOutputType::Geometry),
enable_pbr: Some(true),
..image_builder()
};
assert_eq!(estimate_usd_cents(builder), 13);
}
}
// ── Helpers ──
fn image_builder() -> GenerateMeshRequestBuilder {
GenerateMeshRequestBuilder {
model: RouterMeshModel::Rodin2p5Fast,
provider: RouterProvider::Artcraft,
reference_images: Some(ImageListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_front".to_string()),
])),
..Default::default()
}
}
fn text_builder() -> GenerateMeshRequestBuilder {
GenerateMeshRequestBuilder {
model: RouterMeshModel::Rodin2p5Fast,
provider: RouterProvider::Artcraft,
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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,64 +0,0 @@
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 ArtcraftRodin2p5FastRequestState {
pub request: OmniGenMeshCostAndGenerateRequest,
}
impl ArtcraftRodin2p5FastRequestState {
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::Rodin2p5Fast,
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
}
}
@@ -1,14 +0,0 @@
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::tripo3d_h3p1::request::ArtcraftTripo3dH3p1RequestState;
pub fn build_artcraft_tripo3d_h3p1(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Tripo3dH3p1)?;
let state = ArtcraftTripo3dH3p1RequestState { request };
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftTripo3dH3p1(state)))
}
@@ -1,338 +0,0 @@
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
use enums::common::generation::common_mesh_quality::CommonMeshQuality;
use enums::common::generation::common_polygon_type::CommonPolygonType;
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
use crate::generate::generate_mesh::providers::artcraft::tripo3d_h3p1::request::ArtcraftTripo3dH3p1RequestState;
// Text and multi-view generations share a price tier; single-image
// generations price higher.
const TEXT_OR_MULTIVIEW_UNTEXTURED_COST_IN_USD_CENTS: u64 = 13;
const TEXT_OR_MULTIVIEW_STANDARD_TEXTURE_COST_IN_USD_CENTS: u64 = 26;
const TEXT_OR_MULTIVIEW_DETAILED_TEXTURE_COST_IN_USD_CENTS: u64 = 39;
const IMAGE_UNTEXTURED_COST_IN_USD_CENTS: u64 = 26;
const IMAGE_STANDARD_TEXTURE_COST_IN_USD_CENTS: u64 = 39;
const IMAGE_DETAILED_TEXTURE_COST_IN_USD_CENTS: u64 = 52;
const DETAILED_GEOMETRY_ADD_ON_COST_IN_USD_CENTS: u64 = 26;
const QUAD_ADD_ON_COST_IN_USD_CENTS: u64 = 7;
/// Tripo3D H3.1 via Artcraft: base price by input mode (text and multi-view
/// share a tier; single-image is higher) and texture tier (untextured,
/// standard, or detailed), plus flat add-ons for detailed geometry and quad
/// topology.
#[derive(Clone, Debug)]
pub struct ArtcraftTripo3dH3p1CostState {
pub cost_in_usd_cents: u64,
}
impl ArtcraftTripo3dH3p1CostState {
pub fn from_request(request: &ArtcraftTripo3dH3p1RequestState) -> Self {
let request = &request.request;
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();
let has_primary_image = request.front_image_media_token.is_some()
|| request.reference_image_media_tokens.as_ref().is_some_and(|tokens| !tokens.is_empty());
let uses_single_image = !uses_multi_view && has_primary_image;
// The untextured tier requires that PBR isn't explicitly requested,
// since PBR implies texturing.
let texture_off = (matches!(request.mesh_output_type, Some(CommonMeshOutputType::Geometry))
|| request.enable_texture == Some(false))
&& request.enable_pbr != Some(true);
let (untextured, standard, detailed) = if uses_single_image {
(
IMAGE_UNTEXTURED_COST_IN_USD_CENTS,
IMAGE_STANDARD_TEXTURE_COST_IN_USD_CENTS,
IMAGE_DETAILED_TEXTURE_COST_IN_USD_CENTS,
)
} else {
(
TEXT_OR_MULTIVIEW_UNTEXTURED_COST_IN_USD_CENTS,
TEXT_OR_MULTIVIEW_STANDARD_TEXTURE_COST_IN_USD_CENTS,
TEXT_OR_MULTIVIEW_DETAILED_TEXTURE_COST_IN_USD_CENTS,
)
};
let mut cost = if texture_off {
untextured
} else if matches!(request.texture_quality, Some(CommonMeshQuality::Detailed)) {
detailed
} else {
standard
};
if matches!(request.geometry_quality, Some(CommonMeshQuality::Detailed)) {
cost += DETAILED_GEOMETRY_ADD_ON_COST_IN_USD_CENTS;
}
if matches!(request.polygon_type, Some(CommonPolygonType::Quad)) {
cost += QUAD_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::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 text_mode_costs {
use super::*;
#[test]
fn default_is_standard_textures() {
assert_eq!(estimate_usd_cents(text_builder()), 26);
}
#[test]
fn untextured_via_enable_texture_off() {
let builder = GenerateMeshRequestBuilder {
enable_texture: Some(false),
..text_builder()
};
assert_eq!(estimate_usd_cents(builder), 13);
}
#[test]
fn untextured_via_geometry_output() {
let builder = GenerateMeshRequestBuilder {
mesh_output_type: Some(CommonMeshOutputType::Geometry),
..text_builder()
};
assert_eq!(estimate_usd_cents(builder), 13);
}
#[test]
fn texture_off_with_pbr_keeps_textured_tier() {
let builder = GenerateMeshRequestBuilder {
enable_texture: Some(false),
enable_pbr: Some(true),
..text_builder()
};
assert_eq!(estimate_usd_cents(builder), 26);
}
#[test]
fn detailed_texture() {
let builder = GenerateMeshRequestBuilder {
texture_quality: Some(CommonMeshQuality::Detailed),
..text_builder()
};
assert_eq!(estimate_usd_cents(builder), 39);
}
}
mod image_mode_costs {
use super::*;
#[test]
fn default_is_standard_textures() {
assert_eq!(estimate_usd_cents(image_builder()), 39);
}
#[test]
fn untextured_via_enable_texture_off() {
let builder = GenerateMeshRequestBuilder {
enable_texture: Some(false),
..image_builder()
};
assert_eq!(estimate_usd_cents(builder), 26);
}
#[test]
fn untextured_via_geometry_output() {
let builder = GenerateMeshRequestBuilder {
mesh_output_type: Some(CommonMeshOutputType::Geometry),
..image_builder()
};
assert_eq!(estimate_usd_cents(builder), 26);
}
#[test]
fn detailed_texture() {
let builder = GenerateMeshRequestBuilder {
texture_quality: Some(CommonMeshQuality::Detailed),
..image_builder()
};
assert_eq!(estimate_usd_cents(builder), 52);
}
#[test]
fn front_image_token_prices_as_image_mode() {
let builder = GenerateMeshRequestBuilder {
front_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_front".to_string()))),
..base_builder()
};
assert_eq!(estimate_usd_cents(builder), 39);
}
}
mod multiview_mode_costs {
use super::*;
#[test]
fn default_is_standard_textures() {
assert_eq!(estimate_usd_cents(multiview_builder()), 26);
}
#[test]
fn each_side_view_prices_as_multiview() {
for side in ["back", "left", "right"] {
let mut builder = image_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), 26, "for {side} image");
}
}
#[test]
fn untextured_via_enable_texture_off() {
let builder = GenerateMeshRequestBuilder {
enable_texture: Some(false),
..multiview_builder()
};
assert_eq!(estimate_usd_cents(builder), 13);
}
#[test]
fn detailed_texture() {
let builder = GenerateMeshRequestBuilder {
texture_quality: Some(CommonMeshQuality::Detailed),
..multiview_builder()
};
assert_eq!(estimate_usd_cents(builder), 39);
}
}
mod add_ons {
use super::*;
#[test]
fn detailed_geometry_adds_twenty_six_cents() {
let builder = GenerateMeshRequestBuilder {
geometry_quality: Some(CommonMeshQuality::Detailed),
..image_builder()
};
assert_eq!(estimate_usd_cents(builder), 39 + 26);
}
#[test]
fn quad_adds_seven_cents() {
let builder = GenerateMeshRequestBuilder {
polygon_type: Some(CommonPolygonType::Quad),
..image_builder()
};
assert_eq!(estimate_usd_cents(builder), 39 + 7);
}
#[test]
fn triangle_polygon_adds_nothing() {
let builder = GenerateMeshRequestBuilder {
polygon_type: Some(CommonPolygonType::Triangle),
..image_builder()
};
assert_eq!(estimate_usd_cents(builder), 39);
}
#[test]
fn standard_qualities_add_nothing() {
let builder = GenerateMeshRequestBuilder {
texture_quality: Some(CommonMeshQuality::Standard),
geometry_quality: Some(CommonMeshQuality::Standard),
..image_builder()
};
assert_eq!(estimate_usd_cents(builder), 39);
}
#[test]
fn all_add_ons_stack() {
let builder = GenerateMeshRequestBuilder {
texture_quality: Some(CommonMeshQuality::Detailed),
geometry_quality: Some(CommonMeshQuality::Detailed),
polygon_type: Some(CommonPolygonType::Quad),
..image_builder()
};
assert_eq!(estimate_usd_cents(builder), 52 + 26 + 7);
}
#[test]
fn add_ons_stack_on_the_untextured_tier() {
let builder = GenerateMeshRequestBuilder {
enable_texture: Some(false),
geometry_quality: Some(CommonMeshQuality::Detailed),
polygon_type: Some(CommonPolygonType::Quad),
..text_builder()
};
assert_eq!(estimate_usd_cents(builder), 13 + 26 + 7);
}
}
// ── Helpers ──
fn base_builder() -> GenerateMeshRequestBuilder {
GenerateMeshRequestBuilder {
model: RouterMeshModel::Tripo3dH3p1,
provider: RouterProvider::Artcraft,
..Default::default()
}
}
fn text_builder() -> GenerateMeshRequestBuilder {
GenerateMeshRequestBuilder {
prompt: Some("a red ceramic teapot".to_string()),
..base_builder()
}
}
fn image_builder() -> GenerateMeshRequestBuilder {
GenerateMeshRequestBuilder {
reference_images: Some(ImageListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_front".to_string()),
])),
..base_builder()
}
}
fn multiview_builder() -> GenerateMeshRequestBuilder {
GenerateMeshRequestBuilder {
back_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_back".to_string()))),
..image_builder()
}
}
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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,64 +0,0 @@
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 ArtcraftTripo3dH3p1RequestState {
pub request: OmniGenMeshCostAndGenerateRequest,
}
impl ArtcraftTripo3dH3p1RequestState {
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::Tripo3dH3p1,
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
}
}
@@ -1,330 +0,0 @@
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:?}"),
}
}
}
@@ -1,164 +0,0 @@
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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,141 +0,0 @@
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
}
}
@@ -1,154 +0,0 @@
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()
}
}
}
@@ -1,87 +0,0 @@
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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,68 +0,0 @@
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))
}
}
@@ -1,219 +0,0 @@
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()
}
}
}
@@ -1,75 +0,0 @@
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")
}
}
@@ -1,3 +0,0 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -1,67 +0,0 @@
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))
}
}

Some files were not shown because too many files have changed in this diff Show More