platform-tripo支持图片上传与预览图下载
- 产物下载类型改名为 TripoDownloadedArtifact,模型与预览图共用同一条流式读取路径 - 新增 download_rendered_image,读取 task 结果里的预览图地址 - 新增 upload_image,把图片字节上传 provider 换 file_token - 图片输入收敛为 TripoImageInput 枚举,只有 file_token 与公网地址两种形态,不再接受裸字符串 - smoke 示例跟随新的图片输入类型
This commit is contained in:
@@ -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<dyn std::error::Error>> {
|
||||
|
||||
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,
|
||||
|
||||
@@ -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<TripoDownloadedModel, TripoError> {
|
||||
) -> Result<TripoDownloadedArtifact, TripoError> {
|
||||
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<TripoDownloadedArtifact, TripoError> {
|
||||
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<TripoDownloadedModel, TripoError> {
|
||||
) -> Result<TripoDownloadedArtifact, TripoError> {
|
||||
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<TripoDownloadedModel, TripoError> {
|
||||
) -> Result<TripoDownloadedArtifact, TripoError> {
|
||||
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,
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
pub struct TripoDownloadedModel {
|
||||
/// provider 产物的下载句柄。模型与预览图共用同一条流式读取路径,
|
||||
/// 因此类型名按“产物”而不是“模型”命名。
|
||||
pub struct TripoDownloadedArtifact {
|
||||
pub url: TripoUrl,
|
||||
pub content_type: Option<String>,
|
||||
pub content_length: Option<u64>,
|
||||
@@ -108,7 +119,7 @@ pub struct TripoDownloadedModel {
|
||||
received: u64,
|
||||
}
|
||||
|
||||
impl TripoDownloadedModel {
|
||||
impl TripoDownloadedArtifact {
|
||||
pub(crate) fn new(
|
||||
url: TripoUrl,
|
||||
content_type: Option<String>,
|
||||
|
||||
@@ -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<FileInput, TripoError> {
|
||||
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<u8>,
|
||||
file_name: &str,
|
||||
content_type: &str,
|
||||
) -> Result<TripoImageInput, TripoError> {
|
||||
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<TripoTaskHandle, TripoError> {
|
||||
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,
|
||||
|
||||
@@ -2,4 +2,5 @@ mod client;
|
||||
pub mod result;
|
||||
mod validation;
|
||||
|
||||
pub use client::TripoImageInput;
|
||||
pub use validation::validate_image_to_model_params;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user