diff --git a/server-rs/crates/platform-tripo/examples/tripo_generation_smoke.rs b/server-rs/crates/platform-tripo/examples/tripo_generation_smoke.rs index cf5747e19..6fd8f1b0b 100644 --- a/server-rs/crates/platform-tripo/examples/tripo_generation_smoke.rs +++ b/server-rs/crates/platform-tripo/examples/tripo_generation_smoke.rs @@ -14,7 +14,7 @@ use std::{env, time::Duration}; -use platform_tripo::TripoProviderClient; +use platform_tripo::{TripoImageInput, TripoProviderClient}; use shared_contracts::model3d::{ common::{Model3dModelVersion, Model3dTaskStatus, Model3dTextureQuality}, image_to_model::Model3dImageToModelParams, @@ -73,7 +73,7 @@ async fn main() -> Result<(), Box> { let image = client .submit_image_to_model( - SAMPLE_IMAGE_URL, + &TripoImageInput::PublicUrl(SAMPLE_IMAGE_URL.to_string()), &Model3dImageToModelParams { model: Model3dModelVersion::H31, enable_image_autofix: None, diff --git a/server-rs/crates/platform-tripo/src/common/client.rs b/server-rs/crates/platform-tripo/src/common/client.rs index e4d03bf75..d74e331b5 100644 --- a/server-rs/crates/platform-tripo/src/common/client.rs +++ b/server-rs/crates/platform-tripo/src/common/client.rs @@ -3,8 +3,8 @@ use std::time::Duration; use tripo3d_sdk::TripoClient; use super::{ - TripoDownloadedModel, TripoError, TripoSettings, TripoTaskHandle, TripoTaskSnapshot, TripoUrl, - map_task, validate_task_id, + TripoDownloadedArtifact, TripoError, TripoSettings, TripoTaskHandle, TripoTaskSnapshot, + TripoUrl, map_task, validate_task_id, }; pub struct TripoProviderClient { @@ -46,7 +46,7 @@ impl TripoProviderClient { pub async fn download_model( &self, task: &TripoTaskSnapshot, - ) -> Result { + ) -> Result { let handle = &task.handle; validate_task_id(&handle.task_id)?; let output = task @@ -62,11 +62,32 @@ impl TripoProviderClient { .await } + /// 下载 provider 渲染出的预览图。 + /// + /// 预览图和模型是同一次 task 结果里的两个产物,共用同一条产物下载链路; + /// 命名按“预览图”而不是“渲染图”,避免调用方把它当成模型对象。 + pub async fn download_rendered_image( + &self, + task: &TripoTaskSnapshot, + ) -> Result { + let handle = &task.handle; + validate_task_id(&handle.task_id)?; + let output = task + .output + .as_ref() + .ok_or_else(|| TripoError::OutputSchema { + task_id: handle.task_id.clone(), + message: "task has no completed model output".into(), + })?; + self.download_artifact(&handle.task_id, output.rendered_image_url()) + .await + } + async fn download_artifact( &self, task_id: &str, url: &TripoUrl, - ) -> Result { + ) -> Result { let total_attempts = self.artifact_retries.saturating_add(1); let mut last_error = None; @@ -88,7 +109,7 @@ impl TripoProviderClient { &self, task_id: &str, url: &TripoUrl, - ) -> Result { + ) -> Result { let response = self .artifact_client .get(url.as_str()) @@ -115,7 +136,7 @@ impl TripoProviderClient { .and_then(|value| value.to_str().ok()) .map(str::to_owned); let content_length = response.content_length(); - Ok(TripoDownloadedModel::new( + Ok(TripoDownloadedArtifact::new( url.clone(), content_type, content_length, diff --git a/server-rs/crates/platform-tripo/src/common/mod.rs b/server-rs/crates/platform-tripo/src/common/mod.rs index 810bd6da4..f5ad1094d 100644 --- a/server-rs/crates/platform-tripo/src/common/mod.rs +++ b/server-rs/crates/platform-tripo/src/common/mod.rs @@ -10,7 +10,7 @@ pub use config::TripoSettings; pub use error::{TripoError, TripoField, TripoValidationReason}; pub(crate) use mapping::map_task; pub use types::{ - TripoDownloadedModel, TripoTaskFailure, TripoTaskHandle, TripoTaskOutput, TripoTaskSnapshot, + TripoDownloadedArtifact, TripoTaskFailure, TripoTaskHandle, TripoTaskOutput, TripoTaskSnapshot, TripoTaskType, TripoUrl, }; pub(crate) use validation::{ diff --git a/server-rs/crates/platform-tripo/src/common/types.rs b/server-rs/crates/platform-tripo/src/common/types.rs index c639b8717..516e95f39 100644 --- a/server-rs/crates/platform-tripo/src/common/types.rs +++ b/server-rs/crates/platform-tripo/src/common/types.rs @@ -85,6 +85,15 @@ impl TripoTaskOutput { Self::MultiviewToModel(result) => &result.model_url, } } + + /// 预览图地址;三个端点都返回渲染图,缺失时按输出结构错误处理。 + pub(crate) fn rendered_image_url(&self) -> &TripoUrl { + match self { + Self::TextToModel(result) => &result.rendered_image_url, + Self::ImageToModel(result) => &result.rendered_image_url, + Self::MultiviewToModel(result) => &result.rendered_image_url, + } + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -99,7 +108,9 @@ pub struct TripoTaskSnapshot { pub completed_at: Option, } -pub struct TripoDownloadedModel { +/// provider 产物的下载句柄。模型与预览图共用同一条流式读取路径, +/// 因此类型名按“产物”而不是“模型”命名。 +pub struct TripoDownloadedArtifact { pub url: TripoUrl, pub content_type: Option, pub content_length: Option, @@ -108,7 +119,7 @@ pub struct TripoDownloadedModel { received: u64, } -impl TripoDownloadedModel { +impl TripoDownloadedArtifact { pub(crate) fn new( url: TripoUrl, content_type: Option, diff --git a/server-rs/crates/platform-tripo/src/image_to_model/client.rs b/server-rs/crates/platform-tripo/src/image_to_model/client.rs index 25b773fa2..c9ae60941 100644 --- a/server-rs/crates/platform-tripo/src/image_to_model/client.rs +++ b/server-rs/crates/platform-tripo/src/image_to_model/client.rs @@ -11,22 +11,91 @@ use crate::common::{ use super::validation::validate_image_to_model_params; +/// provider 能读取的图片输入形态。 +/// +/// 平台层只接受站内引用,所以调用方只会产出这两种形态:先上传拿 `file_token`, +/// 或给出公网可读地址。不提供裸字符串变体,避免 SDK 把字符串按前缀推断成 task_id。 +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TripoImageInput { + FileToken(String), + PublicUrl(String), +} + +impl TripoImageInput { + fn to_sdk_file_input(&self) -> Result { + let (value, kind) = match self { + Self::FileToken(value) => (value, "file_token"), + Self::PublicUrl(value) => (value, "url"), + }; + let value = value.trim(); + if value.is_empty() { + return Err(TripoError::InvalidParameters { + field: Some(TripoField::Input), + reason: TripoValidationReason::Required, + message: format!("image {kind} must not be blank"), + }); + } + Ok(match self { + Self::FileToken(_) => FileInput::FileToken(value.to_string()), + Self::PublicUrl(_) => FileInput::Url(value.to_string()), + }) + } +} + impl TripoProviderClient { + /// 把图片字节上传到 provider,换取可直接用于生成任务的 `file_token`。 + /// + /// 站内图片保存在私有 OSS 上,provider 无法直接读取,所以图片输入统一先上传再提交, + /// 不把带签名的临时地址交给第三方。 + pub async fn upload_image( + &self, + bytes: Vec, + file_name: &str, + content_type: &str, + ) -> Result { + let file_name = file_name.trim(); + if file_name.is_empty() { + return Err(TripoError::InvalidParameters { + field: Some(TripoField::Input), + reason: TripoValidationReason::Required, + message: "upload file name must not be blank".into(), + }); + } + if bytes.is_empty() { + return Err(TripoError::InvalidParameters { + field: Some(TripoField::Input), + reason: TripoValidationReason::Required, + message: "upload body must not be empty".into(), + }); + } + let content_type = content_type.trim(); + let uploaded = self + .client + .upload_file( + bytes, + file_name.to_string(), + (!content_type.is_empty()).then_some(content_type), + ) + .await + .map_err(TripoError::from)?; + let token = uploaded.file_token.trim().to_string(); + if token.is_empty() { + return Err(TripoError::OutputSchema { + task_id: "upload".into(), + message: "upload response has no file_token".into(), + }); + } + Ok(TripoImageInput::FileToken(token)) + } + /// 图片输入由调用方解析后传入:provider 只负责把它交给 SDK, /// 站内资源 / 素材的归属校验属于平台层。 pub async fn submit_image_to_model( &self, - input: &str, + input: &TripoImageInput, params: &Model3dImageToModelParams, ) -> Result { - let input = input.trim(); - if input.is_empty() { - return Err(TripoError::InvalidParameters { - field: Some(TripoField::Input), - reason: TripoValidationReason::Required, - message: "input must not be blank".into(), - }); - } + let input = input.to_sdk_file_input()?; validate_image_to_model_params(params)?; let task_id = self @@ -38,9 +107,9 @@ impl TripoProviderClient { } } -fn to_sdk_params(input: &str, params: &Model3dImageToModelParams) -> ImageToModelParams { +fn to_sdk_params(input: FileInput, params: &Model3dImageToModelParams) -> ImageToModelParams { ImageToModelParams { - input: FileInput::from(input), + input, model: Some(params.model.as_str().to_owned()), enable_image_autofix: params.enable_image_autofix, model_seed: params.model_seed, diff --git a/server-rs/crates/platform-tripo/src/image_to_model/mod.rs b/server-rs/crates/platform-tripo/src/image_to_model/mod.rs index e07463772..a4a16bdda 100644 --- a/server-rs/crates/platform-tripo/src/image_to_model/mod.rs +++ b/server-rs/crates/platform-tripo/src/image_to_model/mod.rs @@ -2,4 +2,5 @@ mod client; pub mod result; mod validation; +pub use client::TripoImageInput; pub use validation::validate_image_to_model_params; diff --git a/server-rs/crates/platform-tripo/src/lib.rs b/server-rs/crates/platform-tripo/src/lib.rs index d8284a759..a409c3b36 100644 --- a/server-rs/crates/platform-tripo/src/lib.rs +++ b/server-rs/crates/platform-tripo/src/lib.rs @@ -9,9 +9,9 @@ pub mod multiview_to_model; pub mod text_to_model; pub use common::{ - TripoDownloadedModel, TripoError, TripoField, TripoProviderClient, TripoSettings, + TripoDownloadedArtifact, TripoError, TripoField, TripoProviderClient, TripoSettings, TripoTaskFailure, TripoTaskHandle, TripoTaskOutput, TripoTaskSnapshot, TripoTaskType, TripoUrl, TripoValidationReason, }; -pub use image_to_model::validate_image_to_model_params; +pub use image_to_model::{TripoImageInput, validate_image_to_model_params}; pub use text_to_model::validate_text_to_model_params;