mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-22 09:39:31 -04:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd8fe7f3db | ||
|
|
54b172bf6f | ||
|
|
d7868c7da0 | ||
|
|
d538bc2a68 | ||
|
|
bec8c7ede4 | ||
|
|
0866e63dd4 | ||
|
|
78a5c4ff87 | ||
|
|
d53fad0072 | ||
|
|
2e5e9443a3 | ||
|
|
5bda2a6cb4 |
@@ -77,3 +77,20 @@ 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.
|
||||
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
pub mod omni_gen_audio_cost_and_generate_request;
|
||||
pub mod omni_gen_image_cost_and_generate_request;
|
||||
pub mod omni_gen_mesh_cost_and_generate_request;
|
||||
pub mod omni_gen_splat_cost_and_generate_request;
|
||||
pub mod omni_gen_video_cost_and_generate_request;
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
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>,
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Shared request body for both the splat cost estimate and splat generation endpoints.
|
||||
#[derive(Clone, Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct OmniGenSplatCostAndGenerateRequest {
|
||||
/// REQUIRED (even if marked optional)
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub idempotency_token: Option<String>,
|
||||
|
||||
/// REQUIRED (even if marked optional)
|
||||
/// Which model to use.
|
||||
pub model: Option<CommonSplatModel>,
|
||||
|
||||
/// The text prompt for the splat generation.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Reference images (optional).
|
||||
/// NOTE: Spherical/panorama coordinates are not yet supported per-image;
|
||||
/// multiple images are treated as multi-view input of the same world.
|
||||
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||
|
||||
/// Reference video (optional).
|
||||
pub reference_video_media_token: Option<MediaFileToken>,
|
||||
|
||||
/// Whether the single reference image is a 360-degree panorama.
|
||||
pub is_panoramic: Option<bool>,
|
||||
|
||||
/// Whether to disable prompt recaptioning.
|
||||
pub disable_recaption: Option<bool>,
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod omni_gen_audio_cost_response;
|
||||
pub mod omni_gen_image_cost_response;
|
||||
pub mod omni_gen_mesh_cost_response;
|
||||
pub mod omni_gen_splat_cost_response;
|
||||
pub mod omni_gen_video_cost_response;
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response body for the omni-gen mesh cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniGenMeshCostResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// Estimated cost in credits.
|
||||
pub cost_in_credits: Option<u64>,
|
||||
|
||||
/// Estimated cost in USD cents.
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
|
||||
/// Whether the generation is free for this user/plan.
|
||||
pub is_free: bool,
|
||||
|
||||
/// Whether the user has unlimited generations.
|
||||
pub is_unlimited: bool,
|
||||
|
||||
/// Whether the user is rate-limited.
|
||||
pub is_rate_limited: bool,
|
||||
|
||||
/// Whether the output will have a watermark.
|
||||
pub has_watermark: bool,
|
||||
|
||||
/// Whether failures are refunded.
|
||||
/// True: 100% yes
|
||||
/// False: 100% no
|
||||
/// None: Unknown or variable
|
||||
pub failures_are_refunded: Option<bool>,
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response body for the omni-gen splat cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniGenSplatCostResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// Estimated cost in credits.
|
||||
pub cost_in_credits: Option<u64>,
|
||||
|
||||
/// Estimated cost in USD cents.
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
|
||||
/// Whether the generation is free for this user/plan.
|
||||
pub is_free: bool,
|
||||
|
||||
/// Whether the user has unlimited generations.
|
||||
pub is_unlimited: bool,
|
||||
|
||||
/// Whether the user is rate-limited.
|
||||
pub is_rate_limited: bool,
|
||||
|
||||
/// Whether the output will have a watermark.
|
||||
pub has_watermark: bool,
|
||||
|
||||
/// Whether failures are refunded.
|
||||
/// True: 100% yes
|
||||
/// False: 100% no
|
||||
/// None: Unknown or variable
|
||||
pub failures_are_refunded: Option<bool>,
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response body for the omni-gen video cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniGenVideoCostResponse {
|
||||
pub success: bool,
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod omni_gen_audio_generate_response;
|
||||
pub mod omni_gen_image_generate_response;
|
||||
pub mod omni_gen_mesh_generate_response;
|
||||
pub mod omni_gen_splat_generate_response;
|
||||
pub mod omni_gen_video_generate_response;
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response body for the omni-gen mesh generation endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniGenMeshGenerateResponse {
|
||||
pub success: bool,
|
||||
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
|
||||
/// All job tokens created by this request (including the primary).
|
||||
/// For single-job requests this will contain just one element matching
|
||||
/// `inference_job_token`. For batch requests this will contain all of them.
|
||||
pub all_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response body for the omni-gen splat generation endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniGenSplatGenerateResponse {
|
||||
pub success: bool,
|
||||
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
|
||||
/// All job tokens created by this request (including the primary).
|
||||
/// For single-job requests this will contain just one element matching
|
||||
/// `inference_job_token`. For batch requests this will contain all of them.
|
||||
pub all_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod omni_gen_audio_models;
|
||||
pub mod omni_gen_image_models;
|
||||
pub mod omni_gen_mesh_models;
|
||||
pub mod omni_gen_splat_models;
|
||||
pub mod omni_gen_video_models;
|
||||
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use serde_derive::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Splat model to default to if none is specified
|
||||
const DEFAULT_SPLAT_MODEL : CommonSplatModel = CommonSplatModel::Marble1p1;
|
||||
|
||||
/// Response body for the splat models endpoint.
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenSplatModelsResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// A list of all models: details, features, and capabilities
|
||||
pub models: Vec<OmniGenSplatModelDetails>,
|
||||
|
||||
/// Provider-by-provider model offering and capability list,
|
||||
/// with possible capability overrides (future)
|
||||
pub providers: Vec<OmniGenSplatModelProviderDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenSplatModelProviderDetails {
|
||||
pub provider: GenerationProvider,
|
||||
pub models: Vec<OmniGenSplatProviderModelDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenSplatProviderModelDetails {
|
||||
pub model: CommonSplatModel,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub overrides: Option<OmniGenSplatModelDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Clone)]
|
||||
pub struct OmniGenSplatModelDetails {
|
||||
|
||||
pub model: CommonSplatModel,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model_creator: Option<ModelCreator>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub full_name: Option<String>,
|
||||
|
||||
/// Additional details about the model.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_info: Option<String>,
|
||||
|
||||
/// Additional details about the model. (Brief; only a few words.)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_info_short: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text_prompt_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_references_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_references_max: Option<u16>,
|
||||
|
||||
/// Whether a reference video input is supported.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub video_reference_supported: Option<bool>,
|
||||
|
||||
/// Whether a 360-degree panorama reference image is supported.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub panorama_supported: Option<bool>,
|
||||
|
||||
/// Whether the "disable recaption" toggle is supported.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub disable_recaption_supported: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_disabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for OmniGenSplatModelDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: DEFAULT_SPLAT_MODEL,
|
||||
model_creator: None,
|
||||
full_name: None,
|
||||
extra_info: None,
|
||||
extra_info_short: None,
|
||||
text_prompt_supported: None,
|
||||
image_references_supported: None,
|
||||
image_references_max: None,
|
||||
video_reference_supported: None,
|
||||
panorama_supported: None,
|
||||
disable_recaption_supported: None,
|
||||
is_disabled: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod omni_gen_mesh;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_mesh_generate_response::OmniGenMeshGenerateResponse;
|
||||
|
||||
pub const OMNI_GEN_MESH_GENERATE_PATH: &str = "/v1/omni_gen/generate/mesh";
|
||||
|
||||
pub async fn omni_gen_mesh_generate(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: OmniGenMeshCostAndGenerateRequest,
|
||||
) -> Result<OmniGenMeshGenerateResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
OMNI_GEN_MESH_GENERATE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod audio;
|
||||
pub mod image;
|
||||
pub mod mesh;
|
||||
pub mod splat;
|
||||
pub mod video;
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod omni_gen_splat;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_splat_cost_and_generate_request::OmniGenSplatCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_splat_generate_response::OmniGenSplatGenerateResponse;
|
||||
|
||||
pub const OMNI_GEN_SPLAT_GENERATE_PATH: &str = "/v1/omni_gen/generate/splat";
|
||||
|
||||
pub async fn omni_gen_splat_generate(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: OmniGenSplatCostAndGenerateRequest,
|
||||
) -> Result<OmniGenSplatGenerateResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
OMNI_GEN_SPLAT_GENERATE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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,13 +2,16 @@ 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;
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
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,4 +9,5 @@ pub enum RouterProvider {
|
||||
GmiCloud,
|
||||
GrokApi,
|
||||
Seedance2Pro,
|
||||
WorldLabs,
|
||||
}
|
||||
|
||||
@@ -5,9 +5,68 @@ use serde_derive::{Deserialize, Serialize};
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RouterSplatModel {
|
||||
/// Deprecated upstream; treated as [`Self::Marble1p0Draft`].
|
||||
#[serde(rename = "marble_0p1_mini")]
|
||||
Marble0p1Mini,
|
||||
|
||||
/// Deprecated upstream; treated as [`Self::Marble1p0`].
|
||||
#[serde(rename = "marble_0p1_plus")]
|
||||
Marble0p1Plus,
|
||||
|
||||
#[serde(rename = "marble_1p0")]
|
||||
Marble1p0,
|
||||
|
||||
#[serde(rename = "marble_1p0_draft")]
|
||||
Marble1p0Draft,
|
||||
|
||||
#[serde(rename = "marble_1p1")]
|
||||
Marble1p1,
|
||||
|
||||
#[serde(rename = "marble_1p1_plus")]
|
||||
Marble1p1Plus,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// NB: These strings must match `CommonSplatModel` — the two enums convert
|
||||
// via serde string round-trip.
|
||||
#[test]
|
||||
fn all_variants_serialize_to_common_splat_model_strings() {
|
||||
assert_serde_round_trip(RouterSplatModel::Marble0p1Mini, "marble_0p1_mini");
|
||||
assert_serde_round_trip(RouterSplatModel::Marble0p1Plus, "marble_0p1_plus");
|
||||
assert_serde_round_trip(RouterSplatModel::Marble1p0, "marble_1p0");
|
||||
assert_serde_round_trip(RouterSplatModel::Marble1p0Draft, "marble_1p0_draft");
|
||||
assert_serde_round_trip(RouterSplatModel::Marble1p1, "marble_1p1");
|
||||
assert_serde_round_trip(RouterSplatModel::Marble1p1Plus, "marble_1p1_plus");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_through_common_splat_model() {
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel;
|
||||
|
||||
let cases = [
|
||||
(RouterSplatModel::Marble0p1Mini, CommonSplatModel::Marble0p1Mini),
|
||||
(RouterSplatModel::Marble0p1Plus, CommonSplatModel::Marble0p1Plus),
|
||||
(RouterSplatModel::Marble1p0, CommonSplatModel::Marble1p0),
|
||||
(RouterSplatModel::Marble1p0Draft, CommonSplatModel::Marble1p0Draft),
|
||||
(RouterSplatModel::Marble1p1, CommonSplatModel::Marble1p1),
|
||||
(RouterSplatModel::Marble1p1Plus, CommonSplatModel::Marble1p1Plus),
|
||||
];
|
||||
for (router_model, expected_common) in cases {
|
||||
let json = serde_json::to_string(&router_model).unwrap();
|
||||
let common: CommonSplatModel = serde_json::from_str(&json)
|
||||
.unwrap_or_else(|e| panic!("CommonSplatModel failed to parse {json}: {e}"));
|
||||
assert_eq!(common, expected_common, "for {router_model:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_serde_round_trip(model: RouterSplatModel, expected: &str) {
|
||||
let json = serde_json::to_string(&model).unwrap();
|
||||
assert_eq!(json, format!("\"{expected}\""));
|
||||
let parsed: RouterSplatModel = serde_json::from_str(&json).unwrap();
|
||||
// RouterSplatModel isn't PartialEq, so round-trip back to the wire form.
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum VideoRef {
|
||||
MediaFileToken(MediaFileToken),
|
||||
Url(String),
|
||||
}
|
||||
@@ -8,3 +8,4 @@ pub mod router_fal_client;
|
||||
pub mod router_gmicloud_client;
|
||||
pub mod router_grok_api_client;
|
||||
pub mod router_seedance2pro_client;
|
||||
pub mod router_worldlabs_client;
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
|
||||
pub struct MultiRouterClient {
|
||||
@@ -11,6 +12,7 @@ pub struct MultiRouterClient {
|
||||
pub(crate) gmicloud_client: Option<RouterGmiCloudClient>,
|
||||
pub(crate) grok_api_client: Option<RouterGrokApiClient>,
|
||||
pub(crate) seedance2pro_client: Option<RouterSeedance2ProClient>,
|
||||
pub(crate) worldlabs_client: Option<RouterWorldLabsClient>,
|
||||
}
|
||||
|
||||
impl MultiRouterClient {
|
||||
@@ -38,4 +40,9 @@ impl MultiRouterClient {
|
||||
self.seedance2pro_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::Seedance2Pro))
|
||||
}
|
||||
|
||||
pub fn get_worldlabs_client_ref(&self) -> Result<&RouterWorldLabsClient, ClientError> {
|
||||
self.worldlabs_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::WorldLabs))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
|
||||
|
||||
pub struct MultiRouterClientBuilder {
|
||||
artcraft_client: Option<RouterArtcraftClient>,
|
||||
@@ -11,6 +12,7 @@ pub struct MultiRouterClientBuilder {
|
||||
gmicloud_client: Option<RouterGmiCloudClient>,
|
||||
grok_api_client: Option<RouterGrokApiClient>,
|
||||
seedance2pro_client: Option<RouterSeedance2ProClient>,
|
||||
worldlabs_client: Option<RouterWorldLabsClient>,
|
||||
}
|
||||
|
||||
impl MultiRouterClientBuilder {
|
||||
@@ -21,6 +23,7 @@ impl MultiRouterClientBuilder {
|
||||
gmicloud_client: None,
|
||||
grok_api_client: None,
|
||||
seedance2pro_client: None,
|
||||
worldlabs_client: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +52,11 @@ impl MultiRouterClientBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_worldlabs_client(mut self, client: RouterWorldLabsClient) -> Self {
|
||||
self.worldlabs_client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> MultiRouterClient {
|
||||
MultiRouterClient {
|
||||
artcraft_client: self.artcraft_client,
|
||||
@@ -56,6 +64,7 @@ impl MultiRouterClientBuilder {
|
||||
gmicloud_client: self.gmicloud_client,
|
||||
grok_api_client: self.grok_api_client,
|
||||
seedance2pro_client: self.seedance2pro_client,
|
||||
worldlabs_client: self.worldlabs_client,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
|
||||
pub enum RouterClient {
|
||||
@@ -13,6 +14,7 @@ pub enum RouterClient {
|
||||
GmiCloud(RouterGmiCloudClient),
|
||||
GrokApi(RouterGrokApiClient),
|
||||
Seedance2Pro(RouterSeedance2ProClient),
|
||||
WorldLabs(RouterWorldLabsClient),
|
||||
}
|
||||
|
||||
impl RouterClient {
|
||||
@@ -55,4 +57,12 @@ impl RouterClient {
|
||||
_ => Err(ClientError::ClientNotConfigured(ClientType::Seedance2Pro)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_worldlabs_client_ref(&self) -> Result<&RouterWorldLabsClient, ClientError> {
|
||||
match self {
|
||||
RouterClient::WorldLabs(client) => Ok(client),
|
||||
RouterClient::Multi(multi) => multi.get_worldlabs_client_ref(),
|
||||
_ => Err(ClientError::ClientNotConfigured(ClientType::WorldLabs)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use worldlabs_api_client::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
|
||||
/// A client for the official World Labs (Marble) API.
|
||||
pub struct RouterWorldLabsClient {
|
||||
pub(crate) creds: WorldLabsApiCreds,
|
||||
}
|
||||
|
||||
impl RouterWorldLabsClient {
|
||||
pub fn new(creds: WorldLabsApiCreds) -> Self {
|
||||
Self { creds }
|
||||
}
|
||||
|
||||
pub fn new_from_raw_key(api_key: &str) -> Self {
|
||||
Self::new(WorldLabsApiCreds::new(api_key.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub enum ClientType {
|
||||
GmiCloud,
|
||||
GrokApi,
|
||||
Seedance2Pro,
|
||||
WorldLabs,
|
||||
}
|
||||
|
||||
impl Display for ClientType {
|
||||
@@ -20,6 +21,7 @@ impl Display for ClientType {
|
||||
Self::GmiCloud => write!(f, "GmiCloud"),
|
||||
Self::GrokApi => write!(f, "GrokApi"),
|
||||
Self::Seedance2Pro => write!(f, "Seedance2Pro"),
|
||||
Self::WorldLabs => write!(f, "WorldLabs"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use gmicloud_client::error::gmicloud_error::GmiCloudError;
|
||||
use grok_api_client::error::grok_error::GrokError;
|
||||
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
|
||||
use worldlabs_api_client::error::world_labs_error::WorldLabsError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProviderError {
|
||||
@@ -13,6 +14,7 @@ pub enum ProviderError {
|
||||
GmiCloud(GmiCloudError),
|
||||
Grok(GrokError),
|
||||
Seedance2Pro(Seedance2ProError),
|
||||
WorldLabs(WorldLabsError),
|
||||
}
|
||||
|
||||
impl Error for ProviderError {}
|
||||
@@ -25,6 +27,7 @@ impl Display for ProviderError {
|
||||
Self::GmiCloud(e) => write!(f, "GmiCloud provider error: {}", e),
|
||||
Self::Grok(e) => write!(f, "Grok provider error: {}", e),
|
||||
Self::Seedance2Pro(e) => write!(f, "Seedance2Pro provider error: {}", e),
|
||||
Self::WorldLabs(e) => write!(f, "WorldLabs provider error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,3 +61,9 @@ impl From<Seedance2ProError> for ProviderError {
|
||||
Self::Seedance2Pro(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WorldLabsError> for ProviderError {
|
||||
fn from(error: WorldLabsError) -> Self {
|
||||
Self::WorldLabs(error)
|
||||
}
|
||||
}
|
||||
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftMeshResponsePayload {
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
pub all_inference_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalMeshResponsePayload {
|
||||
pub request_id: Option<String>,
|
||||
pub gateway_request_id: Option<String>,
|
||||
|
||||
/// Queue-mode status URL (fal's queue/polling flow). `None` for webhook
|
||||
/// dispatch — the webhook callback drives status updates instead.
|
||||
pub maybe_status_url: Option<String>,
|
||||
|
||||
/// Queue-mode response URL (fal's queue/polling flow). `None` for webhook
|
||||
/// dispatch.
|
||||
pub maybe_response_url: Option<String>,
|
||||
|
||||
/// The outbound request that was sent to Fal.
|
||||
/// Stored as a trait object so any Request type can be captured.
|
||||
/// Use `format!("{:?}", ...)` or `format!("{:#?}", ...)` to print.
|
||||
pub maybe_outbound_request: Option<Arc<dyn Debug + Send + Sync>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GenerateMeshResponse {
|
||||
Artcraft(ArtcraftMeshResponsePayload),
|
||||
Fal(FalMeshResponsePayload),
|
||||
}
|
||||
|
||||
impl GenerateMeshResponse {
|
||||
pub fn get_artcraft_payload(&self) -> Option<ArtcraftMeshResponsePayload> {
|
||||
match self {
|
||||
Self::Artcraft(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fal_payload(&self) -> Option<FalMeshResponsePayload> {
|
||||
match self {
|
||||
Self::Fal(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/// The result of estimating the cost of a mesh generation plan.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MeshGenerationCostEstimate {
|
||||
pub cost_in_credits: Option<u64>,
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
pub is_free: bool,
|
||||
pub is_unlimited: bool,
|
||||
pub is_rate_limited: bool,
|
||||
pub has_watermark: bool,
|
||||
|
||||
/// Whether failures are refunded.
|
||||
/// True: 100% yes
|
||||
/// False: 100% no
|
||||
/// None: Unknown or variable
|
||||
pub failures_are_refunded: Option<bool>,
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_context::MeshGenerationDraftContext;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
|
||||
/**
|
||||
* Wrapper for all mesh generation draft requests.
|
||||
*
|
||||
* No mesh provider currently needs a draft phase: fal takes media URLs
|
||||
* directly and Artcraft takes media tokens, so `build2()` always returns a
|
||||
* `Request`. The enum exists so the mesh pipeline matches the audio/image
|
||||
* draft-or-request shape and providers that need pre-upload can slot in
|
||||
* later.
|
||||
*/
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MeshGenerationDraftRequest {}
|
||||
|
||||
impl MeshGenerationDraftRequest {
|
||||
|
||||
pub fn get_provider(&self) -> RouterProvider {
|
||||
match *self {}
|
||||
}
|
||||
|
||||
/// Return a cost estimate to fulfill the request.
|
||||
pub fn estimate_cost(&self) -> Result<MeshGenerationCostEstimate, ArtcraftRouterError> {
|
||||
match *self {}
|
||||
}
|
||||
|
||||
/// Finalize the draft request before generation
|
||||
/// This may involve uploading media to the provider.
|
||||
pub async fn finalize(self, _draft_context: MeshGenerationDraftContext<'_>) -> Result<MeshGenerationRequest, ArtcraftRouterError> {
|
||||
match self {}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::client::router_client::RouterClient;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MeshGenerationDraftContext<'a> {
|
||||
/// Optional: the router client, needed for providers that require authentication.
|
||||
pub client: Option<&'a RouterClient>,
|
||||
|
||||
/// Optional context: a map of Media File Tokens to their ArtCraft URLs
|
||||
/// Only needed if we have to fetch these assets and upload them to another provider.
|
||||
pub media_file_to_artcraft_url_map: Option<&'a HashMap<MediaFileToken, String>>,
|
||||
}
|
||||
|
||||
impl Debug for MeshGenerationDraftContext<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("MeshGenerationDraftContext")
|
||||
.field("client", &self.client.is_some())
|
||||
.field("media_file_to_artcraft_url_map", &self.media_file_to_artcraft_url_map)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft::MeshGenerationDraftRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MeshGenerationDraftOrRequest {
|
||||
Draft(MeshGenerationDraftRequest),
|
||||
Request(MeshGenerationRequest),
|
||||
}
|
||||
|
||||
impl MeshGenerationDraftOrRequest {
|
||||
|
||||
pub fn get_provider(&self) -> RouterProvider {
|
||||
match self {
|
||||
Self::Draft(draft) => draft.get_provider(),
|
||||
Self::Request(request) => request.get_provider(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> Result<MeshGenerationCostEstimate, ArtcraftRouterError> {
|
||||
match self {
|
||||
MeshGenerationDraftOrRequest::Draft(draft) => draft.estimate_cost(),
|
||||
MeshGenerationDraftOrRequest::Request(request) => request.estimate_cost(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod generate_mesh_request_builder;
|
||||
pub mod generate_mesh_response;
|
||||
pub mod mesh_generation_cost_estimate;
|
||||
pub mod mesh_generation_draft;
|
||||
pub mod mesh_generation_draft_context;
|
||||
pub mod mesh_generation_draft_or_request;
|
||||
pub mod mesh_generation_request;
|
||||
pub mod providers;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3::request::ArtcraftHunyuan3d3RequestState;
|
||||
|
||||
pub fn build_artcraft_hunyuan3d_3(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d3)?;
|
||||
let state = ArtcraftHunyuan3d3RequestState { request };
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftHunyuan3d3(state)))
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3::request::ArtcraftHunyuan3d3RequestState;
|
||||
|
||||
const NORMAL_COST_IN_USD_CENTS: u64 = 49;
|
||||
const LOW_POLY_COST_IN_USD_CENTS: u64 = 59;
|
||||
const GEOMETRY_COST_IN_USD_CENTS: u64 = 30;
|
||||
const ADD_ON_COST_IN_USD_CENTS: u64 = 20;
|
||||
|
||||
/// Hunyuan 3D v3 via Artcraft: flat base price by output type, plus a flat
|
||||
/// add-on for each extra option (PBR materials, custom face count, and
|
||||
/// multi-view side images).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d3CostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d3CostState {
|
||||
pub fn from_request(request: &ArtcraftHunyuan3d3RequestState) -> Self {
|
||||
let request = &request.request;
|
||||
|
||||
let mut cost = match request.mesh_output_type {
|
||||
None | Some(CommonMeshOutputType::Normal) => NORMAL_COST_IN_USD_CENTS,
|
||||
Some(CommonMeshOutputType::LowPoly) => LOW_POLY_COST_IN_USD_CENTS,
|
||||
Some(CommonMeshOutputType::Geometry) => GEOMETRY_COST_IN_USD_CENTS,
|
||||
};
|
||||
if request.enable_pbr.unwrap_or(false) {
|
||||
cost += ADD_ON_COST_IN_USD_CENTS;
|
||||
}
|
||||
if request.face_count.is_some() {
|
||||
cost += ADD_ON_COST_IN_USD_CENTS;
|
||||
}
|
||||
let uses_multi_view = request.back_image_media_token.is_some()
|
||||
|| request.left_image_media_token.is_some()
|
||||
|| request.right_image_media_token.is_some();
|
||||
if uses_multi_view {
|
||||
cost += ADD_ON_COST_IN_USD_CENTS;
|
||||
}
|
||||
|
||||
Self { cost_in_usd_cents: cost }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
MeshGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_polygon_type::CommonPolygonType;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod base_costs {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_output_type_is_forty_nine_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_is_forty_nine_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Normal),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_poly_is_fifty_nine_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 59);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_is_thirty_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_mode_prices_the_same_as_image_mode() {
|
||||
// Text-only request (no image references).
|
||||
let text_builder = GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(text_builder), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polygon_type_does_not_change_the_price() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
polygon_type: Some(CommonPolygonType::Quad),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49);
|
||||
}
|
||||
}
|
||||
|
||||
mod add_ons {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pbr_adds_twenty_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(true),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbr_false_adds_nothing() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(false),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_count_adds_twenty_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
face_count: Some(100_000),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_side_image_adds_twenty_cents_at_most_once() {
|
||||
for side in ["back", "left", "right"] {
|
||||
let mut builder = base_builder();
|
||||
let image = Some(ImageRef::MediaFileToken(MediaFileToken::new(format!("mf_{side}"))));
|
||||
match side {
|
||||
"back" => builder.back_image = image,
|
||||
"left" => builder.left_image = image,
|
||||
_ => builder.right_image = image,
|
||||
}
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20, "for {side} image");
|
||||
}
|
||||
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_back".to_string()))),
|
||||
left_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_left".to_string()))),
|
||||
right_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_right".to_string()))),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_add_ons_stack() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
enable_pbr: Some(true),
|
||||
face_count: Some(100_000),
|
||||
back_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_back".to_string()))),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 59 + 20 + 20 + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_with_all_add_ons() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
enable_pbr: Some(true),
|
||||
face_count: Some(10_000),
|
||||
left_image: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_left".to_string()))),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 30 + 20 + 20 + 20);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Artcraft,
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_front".to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d3RequestState {
|
||||
pub request: OmniGenMeshCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d3RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::test_helpers::get_artcraft_client;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // sends a real generation to the Artcraft backend, incurs cost
|
||||
async fn text_to_mesh_teapot() {
|
||||
let response = run_pipeline(GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("A red ceramic teapot with a curved spout.".to_string()),
|
||||
..Default::default()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateMeshResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
async fn run_pipeline(builder: GenerateMeshRequestBuilder) -> GenerateMeshResponse {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
MeshGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
|
||||
match &response {
|
||||
GenerateMeshResponse::Artcraft(p) => {
|
||||
println!("inference_job_token={:?}", p.inference_job_token);
|
||||
println!("all_inference_job_tokens={:?}", p.all_inference_job_tokens);
|
||||
}
|
||||
other => println!("response: {:?}", other),
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3_sketch::request::ArtcraftHunyuan3d3SketchRequestState;
|
||||
|
||||
pub fn build_artcraft_hunyuan3d_3_sketch(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d3Sketch)?;
|
||||
let state = ArtcraftHunyuan3d3SketchRequestState { request };
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftHunyuan3d3Sketch(state)))
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan3d_3_sketch::request::ArtcraftHunyuan3d3SketchRequestState;
|
||||
|
||||
const BASE_COST_IN_USD_CENTS: u64 = 49;
|
||||
const ADD_ON_COST_IN_USD_CENTS: u64 = 20;
|
||||
|
||||
/// Hunyuan 3D v3 sketch-to-3D via Artcraft: flat base price plus a flat
|
||||
/// add-on for each extra option (PBR materials, custom face count).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d3SketchCostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d3SketchCostState {
|
||||
pub fn from_request(request: &ArtcraftHunyuan3d3SketchRequestState) -> Self {
|
||||
let request = &request.request;
|
||||
|
||||
let mut cost = BASE_COST_IN_USD_CENTS;
|
||||
if request.enable_pbr.unwrap_or(false) {
|
||||
cost += ADD_ON_COST_IN_USD_CENTS;
|
||||
}
|
||||
if request.face_count.is_some() {
|
||||
cost += ADD_ON_COST_IN_USD_CENTS;
|
||||
}
|
||||
|
||||
Self { cost_in_usd_cents: cost }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
MeshGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
#[test]
|
||||
fn base_cost_is_forty_nine_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbr_adds_twenty_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(true),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbr_false_adds_nothing() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(false),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_count_adds_twenty_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
face_count: Some(100_000),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_add_ons_stack() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(true),
|
||||
face_count: Some(100_000),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 49 + 20 + 20);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3Sketch,
|
||||
provider: RouterProvider::Artcraft,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_sketch".to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d3SketchRequestState {
|
||||
pub request: OmniGenMeshCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d3SketchRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p0::request::ArtcraftHunyuan3d2p0RequestState;
|
||||
|
||||
pub fn build_artcraft_hunyuan_3d_2p0(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d2p0)?;
|
||||
let state = ArtcraftHunyuan3d2p0RequestState { request };
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftHunyuan3d2p0(state)))
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p0::request::ArtcraftHunyuan3d2p0RequestState;
|
||||
|
||||
const TEXTURED_COST_IN_USD_CENTS: u64 = 63;
|
||||
const GEOMETRY_COST_IN_USD_CENTS: u64 = 21;
|
||||
|
||||
/// Hunyuan 3D 2.0 via Artcraft: flat price by output type. Geometry-only
|
||||
/// (white mesh) is cheaper; every other output type prices as textured.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d2p0CostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d2p0CostState {
|
||||
pub fn from_request(request: &ArtcraftHunyuan3d2p0RequestState) -> Self {
|
||||
let cost = match request.request.mesh_output_type {
|
||||
Some(CommonMeshOutputType::Geometry) => GEOMETRY_COST_IN_USD_CENTS,
|
||||
None | Some(_) => TEXTURED_COST_IN_USD_CENTS,
|
||||
};
|
||||
Self { cost_in_usd_cents: cost }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
MeshGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_output_type_is_sixty_three_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 63);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_is_sixty_three_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Normal),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 63);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_is_twenty_one_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 21);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p0,
|
||||
provider: RouterProvider::Artcraft,
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_front".to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d2p0RequestState {
|
||||
pub request: OmniGenMeshCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d2p0RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p1::request::ArtcraftHunyuan3d2p1RequestState;
|
||||
|
||||
pub fn build_artcraft_hunyuan_3d_2p1(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_mesh_request(builder, CommonMeshModelEnum::Hunyuan3d2p1)?;
|
||||
let state = ArtcraftHunyuan3d2p1RequestState { request };
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::ArtcraftHunyuan3d2p1(state)))
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_2p1::request::ArtcraftHunyuan3d2p1RequestState;
|
||||
|
||||
const TEXTURED_COST_IN_USD_CENTS: u64 = 117;
|
||||
const GEOMETRY_COST_IN_USD_CENTS: u64 = 39;
|
||||
|
||||
/// Hunyuan 3D 2.1 via Artcraft: flat price by output type. Geometry-only
|
||||
/// (white mesh) is cheaper; every other output type prices as textured.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d2p1CostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d2p1CostState {
|
||||
pub fn from_request(request: &ArtcraftHunyuan3d2p1RequestState) -> Self {
|
||||
let cost = match request.request.mesh_output_type {
|
||||
Some(CommonMeshOutputType::Geometry) => GEOMETRY_COST_IN_USD_CENTS,
|
||||
None | Some(_) => TEXTURED_COST_IN_USD_CENTS,
|
||||
};
|
||||
Self { cost_in_usd_cents: cost }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
MeshGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_output_type_is_one_seventeen_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 117);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_is_one_seventeen_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Normal),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 117);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_is_thirty_nine_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 39);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p1,
|
||||
provider: RouterProvider::Artcraft,
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_front".to_string()),
|
||||
])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHunyuan3d2p1RequestState {
|
||||
pub request: OmniGenMeshCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftHunyuan3d2p1RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_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)))
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct 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
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_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)))
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct 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
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_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)))
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct 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
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::hunyuan_3d_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)))
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct 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
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::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)))
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct 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
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::mesh::omni_gen_mesh::omni_gen_mesh_generate;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::{
|
||||
ArtcraftMeshResponsePayload, GenerateMeshResponse,
|
||||
};
|
||||
|
||||
/// Send a mesh generation request to the Artcraft omni-gen endpoint.
|
||||
/// All Artcraft model request states delegate to this function.
|
||||
pub async fn send_artcraft_omni_mesh_request(
|
||||
request: &OmniGenMeshCostAndGenerateRequest,
|
||||
client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
let response = omni_gen_mesh_generate(
|
||||
&client.api_host,
|
||||
Some(&client.credentials),
|
||||
request.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateMeshResponse::Artcraft(ArtcraftMeshResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
all_inference_job_tokens: response.all_job_tokens,
|
||||
}))
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
//! 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::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)))
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct 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
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use enums::common::generation::common_mesh_model::CommonMeshModel as CommonMeshModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::artcraft::build_common::build_artcraft_omni_mesh_request;
|
||||
use crate::generate::generate_mesh::providers::artcraft::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)))
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::artcraft::request_common::send_artcraft_omni_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct 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
|
||||
}
|
||||
}
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
use enums::common::generation::common_polygon_type::CommonPolygonType;
|
||||
use fal_client::requests::api::mesh::image::hunyuan3d_3_image_to_mesh::api::{
|
||||
Hunyuan3d3ImageToMeshGenerateType, Hunyuan3d3ImageToMeshPolygonType, Hunyuan3d3ImageToMeshRequest,
|
||||
};
|
||||
use fal_client::requests::api::mesh::text::hunyuan3d_3_text_to_mesh::api::{
|
||||
Hunyuan3d3TextToMeshGenerateType, Hunyuan3d3TextToMeshPolygonType, Hunyuan3d3TextToMeshRequest,
|
||||
};
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::request::{
|
||||
FalHunyuan3d3ImageRequestState, FalHunyuan3d3TextRequestState,
|
||||
};
|
||||
use crate::generate::generate_mesh::providers::fal::resolve::{
|
||||
plan_face_count, plan_primary_image_url, plan_side_image_url,
|
||||
};
|
||||
use crate::generate::generate_mesh::providers::reject_unsupported::{
|
||||
reject_unsupported_image_ref, reject_unsupported_option,
|
||||
};
|
||||
|
||||
/// Hunyuan 3D v3 combines fal's image-to-3d and text-to-3d endpoints under a
|
||||
/// single router model. The request shape picks the endpoint: any image input
|
||||
/// (reference images or `front_image`) dispatches to image-to-3d; otherwise a
|
||||
/// prompt dispatches to text-to-3d.
|
||||
pub fn build_fal_hunyuan3d_3(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_fal_hunyuan3d_3_state(builder)?;
|
||||
let request = match state {
|
||||
FalHunyuan3d3State::Image(state) => MeshGenerationRequest::FalHunyuan3d3Image(state),
|
||||
FalHunyuan3d3State::Text(state) => MeshGenerationRequest::FalHunyuan3d3Text(state),
|
||||
};
|
||||
Ok(MeshGenerationDraftOrRequest::Request(request))
|
||||
}
|
||||
|
||||
/// The endpoint selected by the request shape.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum FalHunyuan3d3State {
|
||||
Image(FalHunyuan3d3ImageRequestState),
|
||||
Text(FalHunyuan3d3TextRequestState),
|
||||
}
|
||||
|
||||
pub(crate) fn build_fal_hunyuan3d_3_state(
|
||||
mut builder: GenerateMeshRequestBuilder,
|
||||
) -> Result<FalHunyuan3d3State, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
let maybe_front_url = plan_primary_image_url(
|
||||
builder.reference_images.take(),
|
||||
builder.front_image.take(),
|
||||
strategy,
|
||||
)?;
|
||||
let face_count = plan_face_count(builder.face_count, strategy)?;
|
||||
|
||||
match maybe_front_url {
|
||||
Some(image_url) => {
|
||||
// Image mode. The image endpoint has no prompt parameter.
|
||||
reject_unsupported_option("prompt", builder.prompt.as_ref(), strategy)?;
|
||||
|
||||
let request = Hunyuan3d3ImageToMeshRequest {
|
||||
image_url,
|
||||
back_image_url: plan_side_image_url(builder.back_image.take())?,
|
||||
left_image_url: plan_side_image_url(builder.left_image.take())?,
|
||||
right_image_url: plan_side_image_url(builder.right_image.take())?,
|
||||
face_count,
|
||||
generate_type: builder.mesh_output_type.map(to_image_generate_type),
|
||||
polygon_type: builder.polygon_type.map(to_image_polygon_type),
|
||||
enable_pbr: builder.enable_pbr,
|
||||
};
|
||||
Ok(FalHunyuan3d3State::Image(FalHunyuan3d3ImageRequestState { request }))
|
||||
}
|
||||
None => {
|
||||
// Text mode. Multi-view side images only make sense alongside a front
|
||||
// image, so they're unsupported here.
|
||||
reject_unsupported_image_ref("back_image", builder.back_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("left_image", builder.left_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("right_image", builder.right_image.as_ref(), strategy)?;
|
||||
|
||||
let prompt = builder.prompt.take().ok_or_else(|| {
|
||||
ArtcraftRouterError::InvalidInput(
|
||||
"Hunyuan 3D v3 requires an input image or a prompt".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let request = Hunyuan3d3TextToMeshRequest {
|
||||
prompt,
|
||||
face_count,
|
||||
generate_type: builder.mesh_output_type.map(to_text_generate_type),
|
||||
polygon_type: builder.polygon_type.map(to_text_polygon_type),
|
||||
enable_pbr: builder.enable_pbr,
|
||||
};
|
||||
Ok(FalHunyuan3d3State::Text(FalHunyuan3d3TextRequestState { request }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Enum mapping helpers ──
|
||||
|
||||
fn to_image_generate_type(output_type: CommonMeshOutputType) -> Hunyuan3d3ImageToMeshGenerateType {
|
||||
match output_type {
|
||||
CommonMeshOutputType::Normal => Hunyuan3d3ImageToMeshGenerateType::Normal,
|
||||
CommonMeshOutputType::LowPoly => Hunyuan3d3ImageToMeshGenerateType::LowPoly,
|
||||
CommonMeshOutputType::Geometry => Hunyuan3d3ImageToMeshGenerateType::Geometry,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_image_polygon_type(polygon_type: CommonPolygonType) -> Hunyuan3d3ImageToMeshPolygonType {
|
||||
match polygon_type {
|
||||
CommonPolygonType::Triangle => Hunyuan3d3ImageToMeshPolygonType::Triangle,
|
||||
CommonPolygonType::Quad => Hunyuan3d3ImageToMeshPolygonType::Quadrilateral,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_text_generate_type(output_type: CommonMeshOutputType) -> Hunyuan3d3TextToMeshGenerateType {
|
||||
match output_type {
|
||||
CommonMeshOutputType::Normal => Hunyuan3d3TextToMeshGenerateType::Normal,
|
||||
CommonMeshOutputType::LowPoly => Hunyuan3d3TextToMeshGenerateType::LowPoly,
|
||||
CommonMeshOutputType::Geometry => Hunyuan3d3TextToMeshGenerateType::Geometry,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_text_polygon_type(polygon_type: CommonPolygonType) -> Hunyuan3d3TextToMeshPolygonType {
|
||||
match polygon_type {
|
||||
CommonPolygonType::Triangle => Hunyuan3d3TextToMeshPolygonType::Triangle,
|
||||
CommonPolygonType::Quad => Hunyuan3d3TextToMeshPolygonType::Quadrilateral,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
use super::*;
|
||||
|
||||
const FRONT_URL: &str = "https://example.com/front.png";
|
||||
const BACK_URL: &str = "https://example.com/back.png";
|
||||
|
||||
mod shape_dispatch {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reference_image_dispatches_to_image_mode() {
|
||||
let state = build_fal_hunyuan3d_3_state(image_builder()).expect("build");
|
||||
let image = expect_image(state);
|
||||
assert_eq!(image.request.image_url, FRONT_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn front_image_dispatches_to_image_mode() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
reference_images: None,
|
||||
front_image: Some(ImageRef::Url(FRONT_URL.to_string())),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan3d_3_state(builder).expect("build");
|
||||
let image = expect_image(state);
|
||||
assert_eq!(image.request.image_url, FRONT_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_only_dispatches_to_text_mode() {
|
||||
let state = build_fal_hunyuan3d_3_state(text_builder()).expect("build");
|
||||
let text = expect_text(state);
|
||||
assert_eq!(text.request.prompt, "a red ceramic teapot");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neither_image_nor_prompt_is_rejected() {
|
||||
let result = build_fal_hunyuan3d_3_state(base_builder());
|
||||
assert!(matches!(result, Err(ArtcraftRouterError::InvalidInput(_))));
|
||||
}
|
||||
}
|
||||
|
||||
mod image_mode {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn side_images_map_to_multi_view_params() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::Url(BACK_URL.to_string())),
|
||||
left_image: Some(ImageRef::Url("https://example.com/left.png".to_string())),
|
||||
right_image: Some(ImageRef::Url("https://example.com/right.png".to_string())),
|
||||
..image_builder()
|
||||
};
|
||||
let image = expect_image(build_fal_hunyuan3d_3_state(builder).expect("build"));
|
||||
assert_eq!(image.request.back_image_url.as_deref(), Some(BACK_URL));
|
||||
assert_eq!(image.request.left_image_url.as_deref(), Some("https://example.com/left.png"));
|
||||
assert_eq!(image.request.right_image_url.as_deref(), Some("https://example.com/right.png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_map_through() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
polygon_type: Some(CommonPolygonType::Quad),
|
||||
face_count: Some(100_000),
|
||||
enable_pbr: Some(true),
|
||||
..image_builder()
|
||||
};
|
||||
let image = expect_image(build_fal_hunyuan3d_3_state(builder).expect("build"));
|
||||
assert!(matches!(image.request.generate_type, Some(Hunyuan3d3ImageToMeshGenerateType::LowPoly)));
|
||||
assert!(matches!(image.request.polygon_type, Some(Hunyuan3d3ImageToMeshPolygonType::Quadrilateral)));
|
||||
assert_eq!(image.request.face_count, Some(100_000));
|
||||
assert_eq!(image.request.enable_pbr, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_with_image_errors_out_under_error_out() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..image_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan3d_3_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_with_image_is_dropped_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
..image_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan3d_3_state(builder).expect("build"),
|
||||
FalHunyuan3d3State::Image(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_tokens_are_rejected() {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_test123".to_string()),
|
||||
])),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan3d_3_state(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
mod text_mode {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn options_map_through() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
polygon_type: Some(CommonPolygonType::Triangle),
|
||||
face_count: Some(40_000),
|
||||
enable_pbr: Some(false),
|
||||
..text_builder()
|
||||
};
|
||||
let text = expect_text(build_fal_hunyuan3d_3_state(builder).expect("build"));
|
||||
assert!(matches!(text.request.generate_type, Some(Hunyuan3d3TextToMeshGenerateType::Geometry)));
|
||||
assert!(matches!(text.request.polygon_type, Some(Hunyuan3d3TextToMeshPolygonType::Triangle)));
|
||||
assert_eq!(text.request.face_count, Some(40_000));
|
||||
assert_eq!(text.request.enable_pbr, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn side_images_without_front_error_out_under_error_out() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::Url(BACK_URL.to_string())),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..text_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan3d_3_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn side_images_without_front_are_dropped_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::Url(BACK_URL.to_string())),
|
||||
..text_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan3d_3_state(builder).expect("build"),
|
||||
FalHunyuan3d3State::Text(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Fal,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn image_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec![FRONT_URL.to_string()])),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn text_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
..base_builder()
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_image(state: FalHunyuan3d3State) -> FalHunyuan3d3ImageRequestState {
|
||||
match state {
|
||||
FalHunyuan3d3State::Image(image) => image,
|
||||
FalHunyuan3d3State::Text(text) => panic!("expected image mode, got text: {text:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_text(state: FalHunyuan3d3State) -> FalHunyuan3d3TextRequestState {
|
||||
match state {
|
||||
FalHunyuan3d3State::Text(text) => text,
|
||||
FalHunyuan3d3State::Image(image) => panic!("expected text mode, got image: {image:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::request::{
|
||||
FalHunyuan3d3ImageRequestState, FalHunyuan3d3TextRequestState,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d3ImageCostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d3ImageCostState {
|
||||
pub fn from_request(request: &FalHunyuan3d3ImageRequestState) -> Self {
|
||||
// Cost math is owned by fal_client's per-endpoint
|
||||
// `FalRequestCostCalculator` implementations. The router state just
|
||||
// forwards the result so router cost ≡ fal_client cost by construction.
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
fal_mesh_cost_estimate(self.cost_in_usd_cents)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d3TextCostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d3TextCostState {
|
||||
pub fn from_request(request: &FalHunyuan3d3TextRequestState) -> Self {
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
fal_mesh_cost_estimate(self.cost_in_usd_cents)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fal_mesh_cost_estimate(cost_in_usd_cents: u64) -> MeshGenerationCostEstimate {
|
||||
MeshGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
const FRONT_URL: &str = "https://example.com/front.png";
|
||||
|
||||
mod image_mode_costs {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn base_cost_table() {
|
||||
let cases = [
|
||||
(None, 38),
|
||||
(Some(CommonMeshOutputType::Normal), 38),
|
||||
(Some(CommonMeshOutputType::LowPoly), 45),
|
||||
(Some(CommonMeshOutputType::Geometry), 23),
|
||||
];
|
||||
for (output_type, expected) in cases {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: output_type,
|
||||
..image_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), expected, "for {output_type:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_ons_stack() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
enable_pbr: Some(true),
|
||||
face_count: Some(100_000),
|
||||
back_image: Some(ImageRef::Url("https://example.com/back.png".to_string())),
|
||||
..image_builder()
|
||||
};
|
||||
// LowPoly(45) + PBR(15) + face_count(15) + multi_view(15)
|
||||
assert_eq!(estimate_usd_cents(builder), 90);
|
||||
}
|
||||
}
|
||||
|
||||
mod text_mode_costs {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn base_cost_table() {
|
||||
let cases = [
|
||||
(None, 38),
|
||||
(Some(CommonMeshOutputType::Normal), 38),
|
||||
(Some(CommonMeshOutputType::LowPoly), 45),
|
||||
(Some(CommonMeshOutputType::Geometry), 23),
|
||||
];
|
||||
for (output_type, expected) in cases {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: output_type,
|
||||
..text_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), expected, "for {output_type:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_ons_stack() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
enable_pbr: Some(true),
|
||||
face_count: Some(40_000),
|
||||
..text_builder()
|
||||
};
|
||||
// Geometry(23) + PBR(15) + face_count(15)
|
||||
assert_eq!(estimate_usd_cents(builder), 53);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn image_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Fal,
|
||||
reference_images: Some(ImageListRef::Urls(vec![FRONT_URL.to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn text_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Fal,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::api::mesh::image::hunyuan3d_3_image_to_mesh::api::Hunyuan3d3ImageToMeshRequest;
|
||||
use fal_client::requests::api::mesh::text::hunyuan3d_3_text_to_mesh::api::Hunyuan3d3TextToMeshRequest;
|
||||
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::{
|
||||
FalMeshResponsePayload, GenerateMeshResponse,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d3ImageRequestState {
|
||||
/// Final materialized request; ready to fire.
|
||||
pub request: Hunyuan3d3ImageToMeshRequest,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d3ImageRequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_fal_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d3TextRequestState {
|
||||
/// Final materialized request; ready to fire.
|
||||
pub request: Hunyuan3d3TextToMeshRequest,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d3TextRequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_fal_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a mesh request to fal using the client's dispatch mode (webhook or
|
||||
/// queue). Shared by all Fal mesh request states.
|
||||
pub(crate) async fn send_fal_mesh_request<R>(
|
||||
request: &R,
|
||||
client: &RouterFalClient,
|
||||
) -> Result<GenerateMeshResponse, ArtcraftRouterError>
|
||||
where
|
||||
R: FalEndpoint + Clone + Debug + Send + Sync + 'static,
|
||||
{
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
|
||||
let payload = if let Some(webhook_url) = &client.webhook_url {
|
||||
let response = request.send_webhook_request(&client.api_key, webhook_url).await?;
|
||||
FalMeshResponsePayload {
|
||||
request_id: response.request_id,
|
||||
gateway_request_id: response.gateway_request_id,
|
||||
maybe_status_url: None,
|
||||
maybe_response_url: None,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}
|
||||
} else {
|
||||
let response = request.send_queue_request(&client.api_key).await?;
|
||||
FalMeshResponsePayload {
|
||||
request_id: Some(response.request_id),
|
||||
gateway_request_id: None,
|
||||
maybe_status_url: Some(response.status_url),
|
||||
maybe_response_url: Some(response.response_url),
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(GenerateMeshResponse::Fal(payload))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_hunyuan3d_3_image_to_mesh() {
|
||||
let response = run_pipeline(GenerateMeshRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec![ERNEST_SCARED_STUPID_IMAGE_URL.to_string()])),
|
||||
..hunyuan3d_3_builder()
|
||||
}).await;
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_hunyuan3d_3_text_to_mesh() {
|
||||
let response = run_pipeline(GenerateMeshRequestBuilder {
|
||||
prompt: Some("A velociraptor with an open mouth full of sharp teeth.".to_string()),
|
||||
..hunyuan3d_3_builder()
|
||||
}).await;
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn hunyuan3d_3_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3,
|
||||
provider: RouterProvider::Fal,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_fal_client() -> RouterClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
let api_key = FalApiKey::from_str(secret.trim());
|
||||
let webhook_url = "https://example.com/fal-webhook-test".to_string();
|
||||
RouterClient::Fal(RouterFalClient::new_with_webhook(api_key, webhook_url))
|
||||
}
|
||||
|
||||
pub(crate) async fn run_pipeline(builder: GenerateMeshRequestBuilder) -> GenerateMeshResponse {
|
||||
let client = get_fal_client();
|
||||
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
MeshGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Fal mesh skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
println!("response: {:?}", response);
|
||||
response
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
use fal_client::requests::api::mesh::sketch::hunyuan3d_3_sketch_to_mesh::api::Hunyuan3d3SketchToMeshRequest;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3_sketch::request::FalHunyuan3d3SketchRequestState;
|
||||
use crate::generate::generate_mesh::providers::fal::resolve::{
|
||||
plan_face_count, plan_primary_image_url,
|
||||
};
|
||||
use crate::generate::generate_mesh::providers::reject_unsupported::{
|
||||
reject_unsupported_image_ref, reject_unsupported_option,
|
||||
};
|
||||
|
||||
pub fn build_fal_hunyuan3d_3_sketch(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_fal_hunyuan3d_3_sketch_state(builder)?;
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::FalHunyuan3d3Sketch(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn build_fal_hunyuan3d_3_sketch_state(
|
||||
mut builder: GenerateMeshRequestBuilder,
|
||||
) -> Result<FalHunyuan3d3SketchRequestState, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// fal's sketch-to-3d schema has no generate_type/polygon_type parameters,
|
||||
// and no multi-view side images.
|
||||
reject_unsupported_option("mesh_output_type", builder.mesh_output_type.as_ref(), strategy)?;
|
||||
reject_unsupported_option("polygon_type", builder.polygon_type.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("back_image", builder.back_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("left_image", builder.left_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("right_image", builder.right_image.as_ref(), strategy)?;
|
||||
|
||||
let image_url = plan_primary_image_url(
|
||||
builder.reference_images.take(),
|
||||
builder.front_image.take(),
|
||||
strategy,
|
||||
)?
|
||||
.ok_or_else(|| ArtcraftRouterError::InvalidInput(
|
||||
"A sketch image is required for Hunyuan 3D v3 sketch-to-3D".to_string(),
|
||||
))?;
|
||||
|
||||
let prompt = builder.prompt.take().ok_or_else(|| {
|
||||
ArtcraftRouterError::InvalidInput(
|
||||
"A prompt is required for Hunyuan 3D v3 sketch-to-3D".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let request = Hunyuan3d3SketchToMeshRequest {
|
||||
image_url,
|
||||
prompt,
|
||||
face_count: plan_face_count(builder.face_count, strategy)?,
|
||||
enable_pbr: builder.enable_pbr,
|
||||
};
|
||||
|
||||
Ok(FalHunyuan3d3SketchRequestState { request })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
use enums::common::generation::common_polygon_type::CommonPolygonType;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
|
||||
use super::*;
|
||||
|
||||
const SKETCH_URL: &str = "https://example.com/sketch.png";
|
||||
|
||||
#[test]
|
||||
fn sketch_and_prompt_pass_through() {
|
||||
let state = build_fal_hunyuan3d_3_sketch_state(base_builder()).expect("build");
|
||||
assert_eq!(state.request.image_url, SKETCH_URL);
|
||||
assert_eq!(state.request.prompt, "a red ceramic teapot");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_sketch_is_rejected() {
|
||||
let builder = GenerateMeshRequestBuilder { reference_images: None, ..base_builder() };
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan3d_3_sketch_state(builder),
|
||||
Err(ArtcraftRouterError::InvalidInput(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_is_rejected() {
|
||||
let builder = GenerateMeshRequestBuilder { prompt: None, ..base_builder() };
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan3d_3_sketch_state(builder),
|
||||
Err(ArtcraftRouterError::InvalidInput(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_count_and_pbr_pass_through() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
face_count: Some(100_000),
|
||||
enable_pbr: Some(true),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan3d_3_sketch_state(builder).expect("build");
|
||||
assert_eq!(state.request.face_count, Some(100_000));
|
||||
assert_eq!(state.request.enable_pbr, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_error_out() {
|
||||
let cases: Vec<GenerateMeshRequestBuilder> = vec![
|
||||
GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
..base_builder()
|
||||
},
|
||||
GenerateMeshRequestBuilder {
|
||||
polygon_type: Some(CommonPolygonType::Quad),
|
||||
..base_builder()
|
||||
},
|
||||
GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::Url("https://example.com/back.png".to_string())),
|
||||
..base_builder()
|
||||
},
|
||||
];
|
||||
for mut builder in cases {
|
||||
builder.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut;
|
||||
assert!(build_fal_hunyuan3d_3_sketch_state(builder).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_are_dropped_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
polygon_type: Some(CommonPolygonType::Quad),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan3d_3_sketch_state(builder).is_ok());
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3Sketch,
|
||||
provider: RouterProvider::Fal,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
reference_images: Some(ImageListRef::Urls(vec![SKETCH_URL.to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::cost::fal_mesh_cost_estimate;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3_sketch::request::FalHunyuan3d3SketchRequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d3SketchCostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d3SketchCostState {
|
||||
pub fn from_request(request: &FalHunyuan3d3SketchRequestState) -> Self {
|
||||
// Cost math is owned by fal_client's per-endpoint
|
||||
// `FalRequestCostCalculator` implementations. The router state just
|
||||
// forwards the result so router cost ≡ fal_client cost by construction.
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
fal_mesh_cost_estimate(self.cost_in_usd_cents)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
#[test]
|
||||
fn base_cost_is_thirty_eight_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 38);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pbr_adds_fifteen_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(true),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 38 + 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face_count_adds_fifteen_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
face_count: Some(100_000),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 38 + 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_add_ons_stack() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
enable_pbr: Some(true),
|
||||
face_count: Some(100_000),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 38 + 15 + 15);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3Sketch,
|
||||
provider: RouterProvider::Fal,
|
||||
prompt: Some("a red ceramic teapot".to_string()),
|
||||
reference_images: Some(ImageListRef::Urls(vec!["https://example.com/sketch.png".to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
use fal_client::requests::api::mesh::sketch::hunyuan3d_3_sketch_to_mesh::api::Hunyuan3d3SketchToMeshRequest;
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::request::send_fal_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d3SketchRequestState {
|
||||
/// Final materialized request; ready to fire.
|
||||
pub request: Hunyuan3d3SketchToMeshRequest,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d3SketchRequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_fal_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_sketch_to_mesh() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d3Sketch,
|
||||
provider: RouterProvider::Fal,
|
||||
prompt: Some("A cartoon character, colorful, plastic material".to_string()),
|
||||
reference_images: Some(ImageListRef::Urls(vec![ERNEST_SCARED_STUPID_IMAGE_URL.to_string()])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let client = get_fal_client();
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
MeshGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Fal mesh skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
println!("response: {:?}", response);
|
||||
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn get_fal_client() -> RouterClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
let api_key = FalApiKey::from_str(secret.trim());
|
||||
let webhook_url = "https://example.com/fal-webhook-test".to_string();
|
||||
RouterClient::Fal(RouterFalClient::new_with_webhook(api_key, webhook_url))
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
use fal_client::requests::api::mesh::image::hunyuan_3d_2p0_image_to_mesh::api::Hunyuan3d2p0ImageToMeshRequest;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
use crate::generate::generate_mesh::mesh_generation_request::MeshGenerationRequest;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_2p0::request::FalHunyuan3d2p0RequestState;
|
||||
use crate::generate::generate_mesh::providers::fal::resolve::{
|
||||
plan_primary_image_url, plan_textured_mesh,
|
||||
};
|
||||
use crate::generate::generate_mesh::providers::reject_unsupported::{
|
||||
reject_unsupported_image_ref, reject_unsupported_option,
|
||||
};
|
||||
|
||||
pub fn build_fal_hunyuan_3d_2p0(builder: GenerateMeshRequestBuilder) -> Result<MeshGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let state = build_fal_hunyuan_3d_2p0_state(builder)?;
|
||||
Ok(MeshGenerationDraftOrRequest::Request(MeshGenerationRequest::FalHunyuan3d2p0(state)))
|
||||
}
|
||||
|
||||
pub(crate) fn build_fal_hunyuan_3d_2p0_state(
|
||||
mut builder: GenerateMeshRequestBuilder,
|
||||
) -> Result<FalHunyuan3d2p0RequestState, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Hunyuan 3D 2.0 is image-to-3D only, with a single input image and no
|
||||
// extra mesh options.
|
||||
reject_unsupported_option("prompt", builder.prompt.as_ref(), strategy)?;
|
||||
reject_unsupported_option("polygon_type", builder.polygon_type.as_ref(), strategy)?;
|
||||
reject_unsupported_option("face_count", builder.face_count.as_ref(), strategy)?;
|
||||
reject_unsupported_option("enable_pbr", builder.enable_pbr.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("back_image", builder.back_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("left_image", builder.left_image.as_ref(), strategy)?;
|
||||
reject_unsupported_image_ref("right_image", builder.right_image.as_ref(), strategy)?;
|
||||
|
||||
let image_url = plan_primary_image_url(
|
||||
builder.reference_images.take(),
|
||||
builder.front_image.take(),
|
||||
strategy,
|
||||
)?
|
||||
.ok_or_else(|| ArtcraftRouterError::InvalidInput(
|
||||
"An input image is required for Hunyuan 3D 2.0".to_string(),
|
||||
))?;
|
||||
|
||||
let request = Hunyuan3d2p0ImageToMeshRequest {
|
||||
image_url,
|
||||
textured_mesh: plan_textured_mesh(builder.mesh_output_type, strategy)?,
|
||||
// Tuning knobs stay at the endpoint's defaults.
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
octree_resolution: None,
|
||||
seed: None,
|
||||
};
|
||||
|
||||
Ok(FalHunyuan3d2p0RequestState { request })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
use enums::common::generation::common_polygon_type::CommonPolygonType;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
|
||||
use super::*;
|
||||
|
||||
const IMAGE_URL: &str = "https://example.com/front.png";
|
||||
|
||||
mod image_requirements {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn single_image_passes_through() {
|
||||
let state = build_fal_hunyuan_3d_2p0_state(base_builder()).expect("build");
|
||||
assert_eq!(state.request.image_url, IMAGE_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_image_is_rejected() {
|
||||
let builder = GenerateMeshRequestBuilder { reference_images: None, ..base_builder() };
|
||||
assert!(matches!(
|
||||
build_fal_hunyuan_3d_2p0_state(builder),
|
||||
Err(ArtcraftRouterError::InvalidInput(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_images_error_out() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec![
|
||||
IMAGE_URL.to_string(),
|
||||
"https://example.com/other.png".to_string(),
|
||||
])),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan_3d_2p0_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_images_keep_the_first_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec![
|
||||
IMAGE_URL.to_string(),
|
||||
"https://example.com/other.png".to_string(),
|
||||
])),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan_3d_2p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.image_url, IMAGE_URL);
|
||||
}
|
||||
}
|
||||
|
||||
mod output_type_mapping {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_maps_to_textured() {
|
||||
let state = build_fal_hunyuan_3d_2p0_state(base_builder()).expect("build");
|
||||
assert_eq!(state.request.textured_mesh, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_maps_to_textured() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Normal),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan_3d_2p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.textured_mesh, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_maps_to_white_mesh() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan_3d_2p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.textured_mesh, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_poly_errors_out_under_error_out() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan_3d_2p0_state(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_poly_upgrades_to_textured_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::LowPoly),
|
||||
..base_builder()
|
||||
};
|
||||
let state = build_fal_hunyuan_3d_2p0_state(builder).expect("build");
|
||||
assert_eq!(state.request.textured_mesh, Some(true));
|
||||
}
|
||||
}
|
||||
|
||||
mod unsupported_options {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_error_out() {
|
||||
let cases: Vec<GenerateMeshRequestBuilder> = vec![
|
||||
GenerateMeshRequestBuilder { prompt: Some("a teapot".to_string()), ..base_builder() },
|
||||
GenerateMeshRequestBuilder { polygon_type: Some(CommonPolygonType::Quad), ..base_builder() },
|
||||
GenerateMeshRequestBuilder { face_count: Some(100_000), ..base_builder() },
|
||||
GenerateMeshRequestBuilder { enable_pbr: Some(true), ..base_builder() },
|
||||
GenerateMeshRequestBuilder {
|
||||
back_image: Some(ImageRef::Url("https://example.com/back.png".to_string())),
|
||||
..base_builder()
|
||||
},
|
||||
];
|
||||
for mut builder in cases {
|
||||
builder.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut;
|
||||
assert!(build_fal_hunyuan_3d_2p0_state(builder).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_options_are_dropped_under_lenient_strategies() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
prompt: Some("a teapot".to_string()),
|
||||
face_count: Some(100_000),
|
||||
enable_pbr: Some(true),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(build_fal_hunyuan_3d_2p0_state(builder).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuning_knobs_stay_at_endpoint_defaults() {
|
||||
let state = build_fal_hunyuan_3d_2p0_state(base_builder()).expect("build");
|
||||
assert!(state.request.guidance_scale.is_none());
|
||||
assert!(state.request.num_inference_steps.is_none());
|
||||
assert!(state.request.octree_resolution.is_none());
|
||||
assert!(state.request.seed.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p0,
|
||||
provider: RouterProvider::Fal,
|
||||
reference_images: Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_mesh::mesh_generation_cost_estimate::MeshGenerationCostEstimate;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::cost::fal_mesh_cost_estimate;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan_3d_2p0::request::FalHunyuan3d2p0RequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d2p0CostState {
|
||||
pub cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d2p0CostState {
|
||||
pub fn from_request(request: &FalHunyuan3d2p0RequestState) -> Self {
|
||||
// Cost math is owned by fal_client's per-endpoint
|
||||
// `FalRequestCostCalculator` implementations. The router state just
|
||||
// forwards the result so router cost ≡ fal_client cost by construction.
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> MeshGenerationCostEstimate {
|
||||
fal_mesh_cost_estimate(self.cost_in_usd_cents)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_mesh_output_type::CommonMeshOutputType;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
|
||||
#[test]
|
||||
fn textured_mesh_is_forty_eight_cents() {
|
||||
assert_eq!(estimate_usd_cents(base_builder()), 48);
|
||||
|
||||
let normal = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Normal),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(normal), 48);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_mesh_is_sixteen_cents() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
mesh_output_type: Some(CommonMeshOutputType::Geometry),
|
||||
..base_builder()
|
||||
};
|
||||
assert_eq!(estimate_usd_cents(builder), 16);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateMeshRequestBuilder {
|
||||
GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p0,
|
||||
provider: RouterProvider::Fal,
|
||||
reference_images: Some(ImageListRef::Urls(vec!["https://example.com/front.png".to_string()])),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_usd_cents(builder: GenerateMeshRequestBuilder) -> u64 {
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
.cost_in_usd_cents
|
||||
.expect("cost should be present")
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
use fal_client::requests::api::mesh::image::hunyuan_3d_2p0_image_to_mesh::api::Hunyuan3d2p0ImageToMeshRequest;
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_mesh::generate_mesh_response::GenerateMeshResponse;
|
||||
use crate::generate::generate_mesh::providers::fal::hunyuan3d_3::request::send_fal_mesh_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalHunyuan3d2p0RequestState {
|
||||
/// Final materialized request; ready to fire.
|
||||
pub request: Hunyuan3d2p0ImageToMeshRequest,
|
||||
}
|
||||
|
||||
impl FalHunyuan3d2p0RequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateMeshResponse, ArtcraftRouterError> {
|
||||
send_fal_mesh_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
|
||||
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::router_mesh_model::RouterMeshModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder;
|
||||
use crate::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_hunyuan_3d_2p0_image_to_mesh() {
|
||||
let builder = GenerateMeshRequestBuilder {
|
||||
model: RouterMeshModel::Hunyuan3d2p0,
|
||||
provider: RouterProvider::Fal,
|
||||
reference_images: Some(ImageListRef::Urls(vec![ERNEST_SCARED_STUPID_IMAGE_URL.to_string()])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let client = get_fal_client();
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
MeshGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Fal mesh skips draft)"),
|
||||
};
|
||||
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
println!("response: {:?}", response);
|
||||
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn get_fal_client() -> RouterClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
let api_key = FalApiKey::from_str(secret.trim());
|
||||
let webhook_url = "https://example.com/fal-webhook-test".to_string();
|
||||
RouterClient::Fal(RouterFalClient::new_with_webhook(api_key, webhook_url))
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user