From 5be4a51ac15f719cd903f2ccf1f3d801477f1d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 18 Sep 2026 17:23:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84Tripo=E6=9E=9A=E4=B8=BE?= =?UTF-8?q?=E4=B8=8E=E5=8F=82=E6=95=B0=E7=BB=84=E5=90=88=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按提供的 Tripo 文档补齐 compression、texture quality 与 texture version 枚举 移除未在三个生成接口出现的 style 占位字段并同步 Rust/TypeScript contracts 增加模型版本、纹理版本、生成部件、quad、smart-low-poly 与 face limit 的副作用前校验 同步 Provider 技术方案和 ADR --- ...ripo SDK隔离与Provider DTO边界-2026-09-18.md | 2 +- ...】Tripo文本到3D模型Provider集成-2026-09-18.md | 4 +- .../model3d/common/Model3dCompression.ts | 2 +- .../model3d/common/Model3dTextureQuality.ts | 6 +- ...del3dStyle.ts => Model3dTextureVersion.ts} | 5 +- .../Model3dImageToModelRequest.ts | 5 +- .../Model3dMultiviewToModelRequest.ts | 3 + .../Model3dTextToModelRequest.ts | 5 +- server-rs/Cargo.lock | 1 + server-rs/crates/platform-tripo/Cargo.toml | 1 + .../crates/platform-tripo/src/common/error.rs | 10 +- .../crates/platform-tripo/src/common/mod.rs | 4 +- .../platform-tripo/src/common/validation.rs | 220 ++++++++++++++++++ .../src/image_to_model/client.rs | 50 +++- .../src/multiview_to_model/client.rs | 70 +++++- .../src/text_to_model/client.rs | 23 +- .../src/text_to_model/validation.rs | 41 +++- .../src/model3d/common/compression.rs | 8 +- .../src/model3d/common/mod.rs | 4 +- .../src/model3d/common/style.rs | 20 -- .../src/model3d/common/texture_quality.rs | 2 + .../src/model3d/common/texture_version.rs | 24 ++ .../src/model3d/image_to_model/request.rs | 8 +- .../src/model3d/multiview_to_model/request.rs | 6 +- .../src/model3d/text_to_model/request.rs | 8 +- 25 files changed, 472 insertions(+), 60 deletions(-) rename packages/shared/src/contracts/model3d/common/{Model3dStyle.ts => Model3dTextureVersion.ts} (53%) delete mode 100644 server-rs/crates/shared-contracts/src/model3d/common/style.rs create mode 100644 server-rs/crates/shared-contracts/src/model3d/common/texture_version.rs diff --git a/docs/adr/【ADR】0001-Tripo SDK隔离与Provider DTO边界-2026-09-18.md b/docs/adr/【ADR】0001-Tripo SDK隔离与Provider DTO边界-2026-09-18.md index 09c6a1fde..6e3f198af 100644 --- a/docs/adr/【ADR】0001-Tripo SDK隔离与Provider DTO边界-2026-09-18.md +++ b/docs/adr/【ADR】0001-Tripo SDK隔离与Provider DTO边界-2026-09-18.md @@ -4,4 +4,4 @@ `tripo3d-sdk` 只允许出现在 `platform-tripo` 内部。该 crate 以显式必填配置创建可复用 provider client,只公开自己的 text-to-model、image-to-model、multiview-to-model DTO、通用单次 task 查询、显式模型下载和结构化错误;不暴露 SDK 类型、`wait_for_task` 或后台轮询。共享产品契约与 provider task 结果分层,provider adapter 不伪造 `resourceId`/`assetId`;shared-contracts 的 Rust 类型始终通过 ts-rs 生成目录化 TypeScript binding。 -选择这个边界是为了避免 API 层绑定第三方 SDK 的任务/错误模型,同时让未来的资源持久化、幂等和 provider 替换由应用层负责。`compress/style` 的正式 enum 和完整组合校验等待 Tripo API 文档,当前只保留编译占位和 TODO,不通过裸字符串逃生。 +选择这个边界是为了避免 API 层绑定第三方 SDK 的任务/错误模型,同时让未来的资源持久化、幂等和 provider 替换由应用层负责。请求 enum 以 Tripo API 文档为准;当前 `compress=geometry`、纹理质量 `fast/standard/detailed/extreme` 及纹理版本已经纳入目录化 Rust/TypeScript contracts。文档确认不存在于这三个 endpoint 的 `style` 字段不再保留占位类型,组合约束在 provider 副作用前执行。 diff --git a/docs/technical/【技术方案】Tripo文本到3D模型Provider集成-2026-09-18.md b/docs/technical/【技术方案】Tripo文本到3D模型Provider集成-2026-09-18.md index 5749346e9..2b643c4da 100644 --- a/docs/technical/【技术方案】Tripo文本到3D模型Provider集成-2026-09-18.md +++ b/docs/technical/【技术方案】Tripo文本到3D模型Provider集成-2026-09-18.md @@ -20,7 +20,9 @@ provider task 结果与产品资源结果分离。provider adapter 不生成 `re ## 请求与类型 -text-to-model 请求包含 SDK 已公开的字段。稳定且已由 SDK 常量定义的字段使用 Rust/TypeScript enum;`compress` 与 `style` 暂时使用带示例 variant 的 enum,并保留官方 API 文档补全 TODO。完整组合校验等待官方 API 文档后一次性落地,不复制不完整的规则集合。 +三个生成请求使用官方文档已确认的 enum:模型版本、纹理版本、纹理质量(含 `fast`)、几何质量、纹理对齐、输入方向、导出方向和压缩类型(`geometry`)。文档未将 `style` 列为这三个 endpoint 的请求字段,因此不再保留占位 enum。 + +提交前由 provider 统一执行组合校验:模型必填;`fast` 必须配 `v3.5-20260815`;几何质量 / 压缩 / 自动尺寸 / P 系列与 H 系列能力按模型版本限制;`quad`、`generate_parts`、`smart_low_poly` 和 `texture/pbr` 组合按文档的互斥关系拒绝;`face_limit` 按模型、quad 和 smart-low-poly 模式检查范围。SDK 返回的参数错误仍统一归一为 `TripoError`。 ## 验收 diff --git a/packages/shared/src/contracts/model3d/common/Model3dCompression.ts b/packages/shared/src/contracts/model3d/common/Model3dCompression.ts index bfde74226..acdd47bf5 100644 --- a/packages/shared/src/contracts/model3d/common/Model3dCompression.ts +++ b/packages/shared/src/contracts/model3d/common/Model3dCompression.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Model3dCompression = 'example'; +export type Model3dCompression = 'geometry'; diff --git a/packages/shared/src/contracts/model3d/common/Model3dTextureQuality.ts b/packages/shared/src/contracts/model3d/common/Model3dTextureQuality.ts index 160606df8..9fe7a47bc 100644 --- a/packages/shared/src/contracts/model3d/common/Model3dTextureQuality.ts +++ b/packages/shared/src/contracts/model3d/common/Model3dTextureQuality.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Model3dTextureQuality = 'standard' | 'detailed' | 'extreme'; +export type Model3dTextureQuality = + | 'fast' + | 'standard' + | 'detailed' + | 'extreme'; diff --git a/packages/shared/src/contracts/model3d/common/Model3dStyle.ts b/packages/shared/src/contracts/model3d/common/Model3dTextureVersion.ts similarity index 53% rename from packages/shared/src/contracts/model3d/common/Model3dStyle.ts rename to packages/shared/src/contracts/model3d/common/Model3dTextureVersion.ts index 49de6c090..b61bff3ab 100644 --- a/packages/shared/src/contracts/model3d/common/Model3dStyle.ts +++ b/packages/shared/src/contracts/model3d/common/Model3dTextureVersion.ts @@ -1,3 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Model3dStyle = 'example'; +export type Model3dTextureVersion = + | 'v3.5-20260815' + | 'v3.0-20250812' + | 'v2.5-20250123'; diff --git a/packages/shared/src/contracts/model3d/image-to-model/Model3dImageToModelRequest.ts b/packages/shared/src/contracts/model3d/image-to-model/Model3dImageToModelRequest.ts index d1a1b17a1..3c140c820 100644 --- a/packages/shared/src/contracts/model3d/image-to-model/Model3dImageToModelRequest.ts +++ b/packages/shared/src/contracts/model3d/image-to-model/Model3dImageToModelRequest.ts @@ -4,9 +4,9 @@ import type { Model3dExportOrientation } from '../common/Model3dExportOrientatio import type { Model3dGeometryQuality } from '../common/Model3dGeometryQuality'; import type { Model3dInputOrientation } from '../common/Model3dInputOrientation'; import type { Model3dModelVersion } from '../common/Model3dModelVersion'; -import type { Model3dStyle } from '../common/Model3dStyle'; import type { Model3dTextureAlignment } from '../common/Model3dTextureAlignment'; import type { Model3dTextureQuality } from '../common/Model3dTextureQuality'; +import type { Model3dTextureVersion } from '../common/Model3dTextureVersion'; export type Model3dImageToModelRequest = { input: string; @@ -17,6 +17,8 @@ export type Model3dImageToModelRequest = { texture: boolean | null; pbr: boolean | null; textureQuality: Model3dTextureQuality | null; + textureVersion: Model3dTextureVersion | null; + delight: boolean | null; textureAlignment: Model3dTextureAlignment | null; geometryQuality: Model3dGeometryQuality | null; faceLimit: number | null; @@ -28,5 +30,4 @@ export type Model3dImageToModelRequest = { compress: Model3dCompression | null; exportUv: boolean | null; exportOrientation: Model3dExportOrientation | null; - style: Model3dStyle | null; }; diff --git a/packages/shared/src/contracts/model3d/multiview-to-model/Model3dMultiviewToModelRequest.ts b/packages/shared/src/contracts/model3d/multiview-to-model/Model3dMultiviewToModelRequest.ts index 9caca8182..62efeddc9 100644 --- a/packages/shared/src/contracts/model3d/multiview-to-model/Model3dMultiviewToModelRequest.ts +++ b/packages/shared/src/contracts/model3d/multiview-to-model/Model3dMultiviewToModelRequest.ts @@ -6,6 +6,7 @@ import type { Model3dInputOrientation } from '../common/Model3dInputOrientation' import type { Model3dModelVersion } from '../common/Model3dModelVersion'; import type { Model3dTextureAlignment } from '../common/Model3dTextureAlignment'; import type { Model3dTextureQuality } from '../common/Model3dTextureQuality'; +import type { Model3dTextureVersion } from '../common/Model3dTextureVersion'; import type { Model3dMultiviewInputs } from './Model3dMultiviewInputs'; export type Model3dMultiviewToModelRequest = { @@ -16,6 +17,8 @@ export type Model3dMultiviewToModelRequest = { texture: boolean | null; pbr: boolean | null; textureQuality: Model3dTextureQuality | null; + textureVersion: Model3dTextureVersion | null; + delight: boolean | null; geometryQuality: Model3dGeometryQuality | null; textureAlignment: Model3dTextureAlignment | null; faceLimit: number | null; diff --git a/packages/shared/src/contracts/model3d/text-to-model/Model3dTextToModelRequest.ts b/packages/shared/src/contracts/model3d/text-to-model/Model3dTextToModelRequest.ts index 7064e70b7..f576d2990 100644 --- a/packages/shared/src/contracts/model3d/text-to-model/Model3dTextToModelRequest.ts +++ b/packages/shared/src/contracts/model3d/text-to-model/Model3dTextToModelRequest.ts @@ -3,8 +3,8 @@ import type { Model3dCompression } from '../common/Model3dCompression'; import type { Model3dExportOrientation } from '../common/Model3dExportOrientation'; import type { Model3dGeometryQuality } from '../common/Model3dGeometryQuality'; import type { Model3dModelVersion } from '../common/Model3dModelVersion'; -import type { Model3dStyle } from '../common/Model3dStyle'; import type { Model3dTextureQuality } from '../common/Model3dTextureQuality'; +import type { Model3dTextureVersion } from '../common/Model3dTextureVersion'; export type Model3dTextToModelRequest = { prompt: string; @@ -16,6 +16,8 @@ export type Model3dTextToModelRequest = { texture: boolean | null; pbr: boolean | null; textureQuality: Model3dTextureQuality | null; + textureVersion: Model3dTextureVersion | null; + delight: boolean | null; geometryQuality: Model3dGeometryQuality | null; faceLimit: number | null; autoSize: boolean | null; @@ -25,5 +27,4 @@ export type Model3dTextToModelRequest = { compress: Model3dCompression | null; exportUv: boolean | null; exportOrientation: Model3dExportOrientation | null; - style: Model3dStyle | null; }; diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index 2b29b2e59..7b54fd8a3 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -4159,6 +4159,7 @@ dependencies = [ name = "platform-tripo" version = "0.1.0" dependencies = [ + "serde_json", "shared-contracts", "tripo3d-sdk", "url", diff --git a/server-rs/crates/platform-tripo/Cargo.toml b/server-rs/crates/platform-tripo/Cargo.toml index c7a4b2b4b..471af15d8 100644 --- a/server-rs/crates/platform-tripo/Cargo.toml +++ b/server-rs/crates/platform-tripo/Cargo.toml @@ -6,5 +6,6 @@ license.workspace = true [dependencies] shared-contracts = { workspace = true } +serde_json = { workspace = true } tripo3d-sdk = { workspace = true } url = { workspace = true } diff --git a/server-rs/crates/platform-tripo/src/common/error.rs b/server-rs/crates/platform-tripo/src/common/error.rs index ecec4511c..88ac58c44 100644 --- a/server-rs/crates/platform-tripo/src/common/error.rs +++ b/server-rs/crates/platform-tripo/src/common/error.rs @@ -5,12 +5,15 @@ pub enum TripoField { Prompt, Model, NegativePrompt, + Input, + Inputs, ImageSeed, ModelSeed, TextureSeed, Texture, Pbr, TextureQuality, + TextureVersion, GeometryQuality, FaceLimit, AutoSize, @@ -20,7 +23,7 @@ pub enum TripoField { Compress, ExportUv, ExportOrientation, - Style, + Delight, TaskId, } @@ -30,12 +33,15 @@ impl fmt::Display for TripoField { Self::Prompt => "prompt", Self::Model => "model", Self::NegativePrompt => "negative_prompt", + Self::Input => "input", + Self::Inputs => "inputs", Self::ImageSeed => "image_seed", Self::ModelSeed => "model_seed", Self::TextureSeed => "texture_seed", Self::Texture => "texture", Self::Pbr => "pbr", Self::TextureQuality => "texture_quality", + Self::TextureVersion => "texture_version", Self::GeometryQuality => "geometry_quality", Self::FaceLimit => "face_limit", Self::AutoSize => "auto_size", @@ -45,7 +51,7 @@ impl fmt::Display for TripoField { Self::Compress => "compress", Self::ExportUv => "export_uv", Self::ExportOrientation => "export_orientation", - Self::Style => "style", + Self::Delight => "delight", Self::TaskId => "task_id", }; f.write_str(name) diff --git a/server-rs/crates/platform-tripo/src/common/mod.rs b/server-rs/crates/platform-tripo/src/common/mod.rs index aa1b1f703..e1396d64c 100644 --- a/server-rs/crates/platform-tripo/src/common/mod.rs +++ b/server-rs/crates/platform-tripo/src/common/mod.rs @@ -13,4 +13,6 @@ pub use types::{ TripoDownloadedModel, TripoModelUrl, TripoTaskFailure, TripoTaskHandle, TripoTaskOutput, TripoTaskSnapshot, TripoTaskType, }; -pub(crate) use validation::validate_task_id; +pub(crate) use validation::{ + TripoGenerationOptions, validate_generation_options, validate_task_id, +}; diff --git a/server-rs/crates/platform-tripo/src/common/validation.rs b/server-rs/crates/platform-tripo/src/common/validation.rs index aa3730e06..3ce263d22 100644 --- a/server-rs/crates/platform-tripo/src/common/validation.rs +++ b/server-rs/crates/platform-tripo/src/common/validation.rs @@ -1,5 +1,225 @@ +use shared_contracts::model3d::common::{ + Model3dCompression, Model3dGeometryQuality, Model3dModelVersion, Model3dTextureQuality, + Model3dTextureVersion, +}; + use super::{TripoError, TripoField, TripoValidationReason}; +pub(crate) struct TripoGenerationOptions { + pub model: Option, + pub texture: Option, + pub pbr: Option, + pub texture_quality: Option, + pub texture_version: Option, + pub geometry_quality: Option, + pub face_limit: Option, + pub auto_size: Option, + pub quad: Option, + pub smart_low_poly: Option, + pub generate_parts: Option, + pub compress: Option, +} + +pub(crate) fn validate_generation_options( + options: &TripoGenerationOptions, +) -> Result<(), TripoError> { + let model = options.model.ok_or_else(|| { + invalid( + Some(TripoField::Model), + TripoValidationReason::Required, + "model is required", + ) + })?; + let family = ModelFamily::from(model); + + if options.texture_quality.is_some() && matches!(family, ModelFamily::H25) { + return Err(invalid( + Some(TripoField::TextureQuality), + TripoValidationReason::InvalidCombination, + "texture_quality is not supported by v2.5-20250123", + )); + } + + if options.texture_quality.is_some_and(|quality| { + matches!(quality, Model3dTextureQuality::Fast) + && options.texture_version != Some(Model3dTextureVersion::V35) + }) { + return Err(invalid( + Some(TripoField::TextureQuality), + TripoValidationReason::InvalidCombination, + "texture_quality=fast requires texture_version=v3.5-20260815", + )); + } + + if options.geometry_quality.is_some() && !matches!(family, ModelFamily::H31 | ModelFamily::H30) + { + return Err(invalid( + Some(TripoField::GeometryQuality), + TripoValidationReason::InvalidCombination, + "geometry_quality is only supported by v3.1-20260211 and v3.0-20250812", + )); + } + + if options.compress.is_some() && !matches!(family, ModelFamily::H31 | ModelFamily::H30) { + return Err(invalid( + Some(TripoField::Compress), + TripoValidationReason::InvalidCombination, + "compress is only supported by v3.1-20260211 and v3.0-20250812", + )); + } + + if options.auto_size.is_some() && matches!(family, ModelFamily::H25) { + return Err(invalid( + Some(TripoField::AutoSize), + TripoValidationReason::InvalidCombination, + "auto_size is not supported by v2.5-20250123", + )); + } + + if options + .smart_low_poly + .is_some_and(|enabled| enabled && !matches!(family, ModelFamily::H31 | ModelFamily::H30)) + { + return Err(invalid( + Some(TripoField::SmartLowPoly), + TripoValidationReason::InvalidCombination, + "smart_low_poly is only supported by v3.1-20260211 and v3.0-20250812", + )); + } + + if options + .generate_parts + .is_some_and(|enabled| enabled && !matches!(family, ModelFamily::H31 | ModelFamily::H30)) + { + return Err(invalid( + Some(TripoField::GenerateParts), + TripoValidationReason::InvalidCombination, + "generate_parts is only supported by v3.1-20260211 and v3.0-20250812", + )); + } + + if options.quad == Some(true) + && !matches!( + family, + ModelFamily::H31 | ModelFamily::H30 | ModelFamily::P2 + ) + { + return Err(invalid( + Some(TripoField::Quad), + TripoValidationReason::InvalidCombination, + "quad=true is only supported by v3.x models and P2-20260801", + )); + } + + if options.generate_parts == Some(true) { + if options.texture != Some(false) { + return Err(invalid( + Some(TripoField::GenerateParts), + TripoValidationReason::InvalidCombination, + "generate_parts=true requires texture=false", + )); + } + if options.pbr != Some(false) { + return Err(invalid( + Some(TripoField::GenerateParts), + TripoValidationReason::InvalidCombination, + "generate_parts=true requires pbr=false", + )); + } + if options.quad == Some(true) || options.smart_low_poly == Some(true) { + return Err(invalid( + Some(TripoField::GenerateParts), + TripoValidationReason::InvalidCombination, + "generate_parts=true cannot be combined with quad or smart_low_poly", + )); + } + } + + if let Some(face_limit) = options.face_limit { + let (minimum, maximum) = face_limit_bounds(family, options); + if face_limit < minimum || face_limit > maximum { + return Err(invalid( + Some(TripoField::FaceLimit), + TripoValidationReason::InvalidCombination, + &format!( + "face_limit must be between {minimum} and {maximum} for the selected model and options" + ), + )); + } + } + + Ok(()) +} + +#[derive(Clone, Copy)] +enum ModelFamily { + H25, + H30, + H31, + P1, + P2, +} + +impl From for ModelFamily { + fn from(model: Model3dModelVersion) -> Self { + match model { + Model3dModelVersion::H25 => Self::H25, + Model3dModelVersion::H30 => Self::H30, + Model3dModelVersion::H31 => Self::H31, + Model3dModelVersion::P1 => Self::P1, + Model3dModelVersion::P2 => Self::P2, + } + } +} + +fn face_limit_bounds(family: ModelFamily, options: &TripoGenerationOptions) -> (i64, i64) { + if options.smart_low_poly == Some(true) { + return if options.quad == Some(true) { + (500, 10_000) + } else { + (500, 20_000) + }; + } + + match family { + ModelFamily::H25 => (1, 500_000), + ModelFamily::H30 => { + if options.quad == Some(true) { + (1, 150_000) + } else if options.geometry_quality == Some(Model3dGeometryQuality::Detailed) { + (1, 2_000_000) + } else { + (1, 1_000_000) + } + } + ModelFamily::H31 => { + if options.quad == Some(true) { + (1, 150_000) + } else if options.geometry_quality == Some(Model3dGeometryQuality::Detailed) { + (1, 2_000_000) + } else { + (1, 1_500_000) + } + } + ModelFamily::P1 => (50, 20_000), + ModelFamily::P2 => { + if options.quad == Some(true) { + (48, 25_000) + } else { + (48, 50_000) + } + } + } +} + +fn invalid(field: Option, reason: TripoValidationReason, message: &str) -> TripoError { + TripoError::InvalidParameters { + field, + reason, + message: message.into(), + } +} + pub(crate) fn validate_task_id(task_id: &str) -> Result<(), TripoError> { if task_id.trim().is_empty() { return Err(TripoError::InvalidParameters { 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 f7cfdf71a..d34fbe880 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 @@ -1,11 +1,14 @@ use shared_contracts::model3d::common::{ Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dInputOrientation, - Model3dModelVersion, Model3dStyle, Model3dTextureAlignment, Model3dTextureQuality, + Model3dModelVersion, Model3dTextureAlignment, Model3dTextureQuality, Model3dTextureVersion, }; use shared_contracts::model3d::image_to_model::Model3dImageToModelRequest; use tripo3d_sdk::{models::FileInput, params::ImageToModelParams}; -use crate::common::{TripoError, TripoProviderClient, TripoTaskHandle}; +use crate::common::{ + TripoError, TripoField, TripoGenerationOptions, TripoProviderClient, TripoTaskHandle, + TripoValidationReason, validate_generation_options, +}; impl TripoProviderClient { pub async fn submit_image_to_model( @@ -13,10 +16,26 @@ impl TripoProviderClient { request: &Model3dImageToModelRequest, ) -> Result { if request.input.trim().is_empty() { - return Err(TripoError::SdkInvalidArgument( - "image_to_model: input is required".into(), - )); + return Err(TripoError::InvalidParameters { + field: Some(TripoField::Input), + reason: TripoValidationReason::Required, + message: "input must not be blank".into(), + }); } + validate_generation_options(&TripoGenerationOptions { + model: request.model, + texture: request.texture, + pbr: request.pbr, + texture_quality: request.texture_quality, + texture_version: request.texture_version, + geometry_quality: request.geometry_quality, + face_limit: request.face_limit, + auto_size: request.auto_size, + quad: request.quad, + smart_low_poly: request.smart_low_poly, + generate_parts: request.generate_parts, + compress: request.compress, + })?; let task_id = self .client @@ -43,6 +62,7 @@ fn to_sdk_params(request: &Model3dImageToModelRequest) -> ImageToModelParams { .texture_quality .map(Model3dTextureQuality::as_str) .map(str::to_owned), + extra: extra_fields(request.texture_version, request.delight), texture_alignment: request .texture_alignment .map(Model3dTextureAlignment::as_str) @@ -69,7 +89,23 @@ fn to_sdk_params(request: &Model3dImageToModelRequest) -> ImageToModelParams { .export_orientation .map(Model3dExportOrientation::as_str) .map(str::to_owned), - style: request.style.map(Model3dStyle::as_str).map(str::to_owned), - ..Default::default() + style: None, } } + +fn extra_fields( + texture_version: Option, + delight: Option, +) -> std::collections::HashMap { + let mut extra = std::collections::HashMap::new(); + if let Some(value) = texture_version { + extra.insert( + "texture_version".into(), + serde_json::Value::String(value.as_str().into()), + ); + } + if let Some(value) = delight { + extra.insert("delight".into(), serde_json::Value::Bool(value)); + } + extra +} diff --git a/server-rs/crates/platform-tripo/src/multiview_to_model/client.rs b/server-rs/crates/platform-tripo/src/multiview_to_model/client.rs index 6d321e1b9..e7cc60642 100644 --- a/server-rs/crates/platform-tripo/src/multiview_to_model/client.rs +++ b/server-rs/crates/platform-tripo/src/multiview_to_model/client.rs @@ -7,13 +7,70 @@ use shared_contracts::model3d::multiview_to_model::{ }; use tripo3d_sdk::{models::FileInput, params::MultiviewToModelParams}; -use crate::common::{TripoError, TripoProviderClient, TripoTaskHandle}; +use crate::common::{ + TripoError, TripoField, TripoGenerationOptions, TripoProviderClient, TripoTaskHandle, + TripoValidationReason, validate_generation_options, +}; impl TripoProviderClient { pub async fn submit_multiview_to_model( &self, request: &Model3dMultiviewToModelRequest, ) -> Result { + match &request.inputs { + Model3dMultiviewInputs::Views { + front, + left, + back, + right, + } => { + if front.trim().is_empty() { + return Err(TripoError::InvalidParameters { + field: Some(TripoField::Inputs), + reason: TripoValidationReason::Required, + message: "front view is required".into(), + }); + } + if [left, back, right] + .into_iter() + .filter(|value| { + value + .as_deref() + .is_some_and(|value| !value.trim().is_empty()) + }) + .count() + == 0 + { + return Err(TripoError::InvalidParameters { + field: Some(TripoField::Inputs), + reason: TripoValidationReason::InvalidCombination, + message: "at least two views are required".into(), + }); + } + } + Model3dMultiviewInputs::TaskId { task_id } if task_id.trim().is_empty() => { + return Err(TripoError::InvalidParameters { + field: Some(TripoField::Inputs), + reason: TripoValidationReason::Required, + message: "task_id must not be blank".into(), + }); + } + Model3dMultiviewInputs::TaskId { .. } => {} + } + validate_generation_options(&TripoGenerationOptions { + model: request.model, + texture: request.texture, + pbr: request.pbr, + texture_quality: request.texture_quality, + texture_version: request.texture_version, + geometry_quality: request.geometry_quality, + face_limit: request.face_limit, + auto_size: request.auto_size, + quad: request.quad, + smart_low_poly: request.smart_low_poly, + generate_parts: request.generate_parts, + compress: request.compress, + })?; let task_id = self .client .multiview_to_model(to_sdk_params(request)) @@ -52,6 +109,17 @@ fn to_sdk_params(request: &Model3dMultiviewToModelRequest) -> MultiviewToModelPa .texture_quality .map(Model3dTextureQuality::as_str) .map(str::to_owned); + if let Some(value) = request.texture_version { + params.extra.insert( + "texture_version".into(), + serde_json::Value::String(value.as_str().into()), + ); + } + if let Some(value) = request.delight { + params + .extra + .insert("delight".into(), serde_json::Value::Bool(value)); + } params.geometry_quality = request .geometry_quality .map(Model3dGeometryQuality::as_str) diff --git a/server-rs/crates/platform-tripo/src/text_to_model/client.rs b/server-rs/crates/platform-tripo/src/text_to_model/client.rs index d6b6a6735..cb150e4f1 100644 --- a/server-rs/crates/platform-tripo/src/text_to_model/client.rs +++ b/server-rs/crates/platform-tripo/src/text_to_model/client.rs @@ -1,7 +1,7 @@ use crate::common::{TripoError, TripoProviderClient, TripoTaskHandle}; use shared_contracts::model3d::common::{ Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dModelVersion, - Model3dStyle, Model3dTextureQuality, + Model3dTextureQuality, Model3dTextureVersion, }; use shared_contracts::model3d::text_to_model::Model3dTextToModelRequest; @@ -39,6 +39,7 @@ fn to_sdk_params(request: &Model3dTextToModelRequest) -> tripo3d_sdk::params::Te .texture_quality .map(Model3dTextureQuality::as_str) .map(str::to_owned), + extra: extra_fields(request.texture_version, request.delight), geometry_quality: request .geometry_quality .map(Model3dGeometryQuality::as_str) @@ -57,7 +58,23 @@ fn to_sdk_params(request: &Model3dTextToModelRequest) -> tripo3d_sdk::params::Te .export_orientation .map(Model3dExportOrientation::as_str) .map(str::to_owned), - style: request.style.map(Model3dStyle::as_str).map(str::to_owned), - ..Default::default() + style: None, } } + +fn extra_fields( + texture_version: Option, + delight: Option, +) -> std::collections::HashMap { + let mut extra = std::collections::HashMap::new(); + if let Some(value) = texture_version { + extra.insert( + "texture_version".into(), + serde_json::Value::String(value.as_str().into()), + ); + } + if let Some(value) = delight { + extra.insert("delight".into(), serde_json::Value::Bool(value)); + } + extra +} diff --git a/server-rs/crates/platform-tripo/src/text_to_model/validation.rs b/server-rs/crates/platform-tripo/src/text_to_model/validation.rs index 9fef1edef..96b74b70c 100644 --- a/server-rs/crates/platform-tripo/src/text_to_model/validation.rs +++ b/server-rs/crates/platform-tripo/src/text_to_model/validation.rs @@ -1,6 +1,9 @@ use shared_contracts::model3d::text_to_model::Model3dTextToModelRequest; -use crate::common::{TripoError, TripoField, TripoValidationReason}; +use crate::common::{ + TripoError, TripoField, TripoGenerationOptions, TripoValidationReason, + validate_generation_options, +}; pub(crate) fn validate_text_to_model_request( request: &Model3dTextToModelRequest, @@ -13,7 +16,39 @@ pub(crate) fn validate_text_to_model_request( }); } - // TODO(tripo-docs): add the complete documented cross-field validation - // matrix in one pass when the official text-to-model API contract arrives. + if request.prompt.chars().count() > 1024 { + return Err(TripoError::InvalidParameters { + field: Some(TripoField::Prompt), + reason: TripoValidationReason::InvalidCombination, + message: "prompt must not exceed 1024 characters".into(), + }); + } + if request + .negative_prompt + .as_deref() + .is_some_and(|value| value.chars().count() > 255) + { + return Err(TripoError::InvalidParameters { + field: Some(TripoField::NegativePrompt), + reason: TripoValidationReason::InvalidCombination, + message: "negative_prompt must not exceed 255 characters".into(), + }); + } + + validate_generation_options(&TripoGenerationOptions { + model: request.model, + texture: request.texture, + pbr: request.pbr, + texture_quality: request.texture_quality, + texture_version: request.texture_version, + geometry_quality: request.geometry_quality, + face_limit: request.face_limit, + auto_size: request.auto_size, + quad: request.quad, + smart_low_poly: request.smart_low_poly, + generate_parts: request.generate_parts, + compress: request.compress, + })?; + Ok(()) } diff --git a/server-rs/crates/shared-contracts/src/model3d/common/compression.rs b/server-rs/crates/shared-contracts/src/model3d/common/compression.rs index aeb219989..2fda58d5f 100644 --- a/server-rs/crates/shared-contracts/src/model3d/common/compression.rs +++ b/server-rs/crates/shared-contracts/src/model3d/common/compression.rs @@ -5,16 +5,14 @@ use super::MODEL3D_TS_EXPORT_DIR; #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ts_rs::TS)] #[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)] pub enum Model3dCompression { - /// TODO(tripo-docs): replace this compile-time placeholder with the - /// official text-to-model compression enum values. - #[serde(rename = "example")] - Example, + #[serde(rename = "geometry")] + Geometry, } impl Model3dCompression { pub const fn as_str(self) -> &'static str { match self { - Self::Example => "example", + Self::Geometry => "geometry", } } } diff --git a/server-rs/crates/shared-contracts/src/model3d/common/mod.rs b/server-rs/crates/shared-contracts/src/model3d/common/mod.rs index c54bf2620..a4c3916b8 100644 --- a/server-rs/crates/shared-contracts/src/model3d/common/mod.rs +++ b/server-rs/crates/shared-contracts/src/model3d/common/mod.rs @@ -9,10 +9,10 @@ mod geometry_quality; mod input_orientation; mod model_version; mod output_format; -mod style; mod task_status; mod texture_alignment; mod texture_quality; +mod texture_version; pub use compression::Model3dCompression; pub use export_orientation::Model3dExportOrientation; @@ -20,7 +20,7 @@ pub use geometry_quality::Model3dGeometryQuality; pub use input_orientation::Model3dInputOrientation; pub use model_version::Model3dModelVersion; pub use output_format::Model3dOutputFormat; -pub use style::Model3dStyle; pub use task_status::Model3dTaskStatus; pub use texture_alignment::Model3dTextureAlignment; pub use texture_quality::Model3dTextureQuality; +pub use texture_version::Model3dTextureVersion; diff --git a/server-rs/crates/shared-contracts/src/model3d/common/style.rs b/server-rs/crates/shared-contracts/src/model3d/common/style.rs deleted file mode 100644 index 096cec931..000000000 --- a/server-rs/crates/shared-contracts/src/model3d/common/style.rs +++ /dev/null @@ -1,20 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::MODEL3D_TS_EXPORT_DIR; - -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ts_rs::TS)] -#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)] -pub enum Model3dStyle { - /// TODO(tripo-docs): replace this compile-time placeholder with the - /// official text-to-model style enum values. - #[serde(rename = "example")] - Example, -} - -impl Model3dStyle { - pub const fn as_str(self) -> &'static str { - match self { - Self::Example => "example", - } - } -} diff --git a/server-rs/crates/shared-contracts/src/model3d/common/texture_quality.rs b/server-rs/crates/shared-contracts/src/model3d/common/texture_quality.rs index 661bd1b27..b1f77ac97 100644 --- a/server-rs/crates/shared-contracts/src/model3d/common/texture_quality.rs +++ b/server-rs/crates/shared-contracts/src/model3d/common/texture_quality.rs @@ -7,6 +7,7 @@ use super::MODEL3D_TS_EXPORT_DIR; #[derive(ts_rs::TS)] #[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)] pub enum Model3dTextureQuality { + Fast, Standard, Detailed, Extreme, @@ -15,6 +16,7 @@ pub enum Model3dTextureQuality { impl Model3dTextureQuality { pub const fn as_str(self) -> &'static str { match self { + Self::Fast => "fast", Self::Standard => "standard", Self::Detailed => "detailed", Self::Extreme => "extreme", diff --git a/server-rs/crates/shared-contracts/src/model3d/common/texture_version.rs b/server-rs/crates/shared-contracts/src/model3d/common/texture_version.rs new file mode 100644 index 000000000..847b94c04 --- /dev/null +++ b/server-rs/crates/shared-contracts/src/model3d/common/texture_version.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +use super::MODEL3D_TS_EXPORT_DIR; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ts_rs::TS)] +#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)] +pub enum Model3dTextureVersion { + #[serde(rename = "v3.5-20260815")] + V35, + #[serde(rename = "v3.0-20250812")] + V30, + #[serde(rename = "v2.5-20250123")] + V25, +} + +impl Model3dTextureVersion { + pub const fn as_str(self) -> &'static str { + match self { + Self::V35 => "v3.5-20260815", + Self::V30 => "v3.0-20250812", + Self::V25 => "v2.5-20250123", + } + } +} diff --git a/server-rs/crates/shared-contracts/src/model3d/image_to_model/request.rs b/server-rs/crates/shared-contracts/src/model3d/image_to_model/request.rs index fd2233640..662f905c7 100644 --- a/server-rs/crates/shared-contracts/src/model3d/image_to_model/request.rs +++ b/server-rs/crates/shared-contracts/src/model3d/image_to_model/request.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::model3d::common::{ Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dInputOrientation, - Model3dModelVersion, Model3dStyle, Model3dTextureAlignment, Model3dTextureQuality, + Model3dModelVersion, Model3dTextureAlignment, Model3dTextureQuality, Model3dTextureVersion, }; use super::MODEL3D_TS_EXPORT_DIR; @@ -30,6 +30,10 @@ pub struct Model3dImageToModelRequest { #[serde(default)] pub texture_quality: Option, #[serde(default)] + pub texture_version: Option, + #[serde(default)] + pub delight: Option, + #[serde(default)] pub texture_alignment: Option, #[serde(default)] pub geometry_quality: Option, @@ -52,6 +56,4 @@ pub struct Model3dImageToModelRequest { pub export_uv: Option, #[serde(default)] pub export_orientation: Option, - #[serde(default)] - pub style: Option, } diff --git a/server-rs/crates/shared-contracts/src/model3d/multiview_to_model/request.rs b/server-rs/crates/shared-contracts/src/model3d/multiview_to_model/request.rs index c98ded2d0..83fec10e8 100644 --- a/server-rs/crates/shared-contracts/src/model3d/multiview_to_model/request.rs +++ b/server-rs/crates/shared-contracts/src/model3d/multiview_to_model/request.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::model3d::common::{ Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dInputOrientation, - Model3dModelVersion, Model3dTextureAlignment, Model3dTextureQuality, + Model3dModelVersion, Model3dTextureAlignment, Model3dTextureQuality, Model3dTextureVersion, }; use super::MODEL3D_TS_EXPORT_DIR; @@ -48,6 +48,10 @@ pub struct Model3dMultiviewToModelRequest { #[serde(default)] pub texture_quality: Option, #[serde(default)] + pub texture_version: Option, + #[serde(default)] + pub delight: Option, + #[serde(default)] pub geometry_quality: Option, #[serde(default)] pub texture_alignment: Option, diff --git a/server-rs/crates/shared-contracts/src/model3d/text_to_model/request.rs b/server-rs/crates/shared-contracts/src/model3d/text_to_model/request.rs index 993b82cc3..77cdfc881 100644 --- a/server-rs/crates/shared-contracts/src/model3d/text_to_model/request.rs +++ b/server-rs/crates/shared-contracts/src/model3d/text_to_model/request.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use super::MODEL3D_TS_EXPORT_DIR; use crate::model3d::common::{ Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dModelVersion, - Model3dStyle, Model3dTextureQuality, + Model3dTextureQuality, Model3dTextureVersion, }; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -32,6 +32,10 @@ pub struct Model3dTextToModelRequest { #[serde(default)] pub texture_quality: Option, #[serde(default)] + pub texture_version: Option, + #[serde(default)] + pub delight: Option, + #[serde(default)] pub geometry_quality: Option, #[serde(default)] #[ts(type = "number | null")] @@ -50,6 +54,4 @@ pub struct Model3dTextToModelRequest { pub export_uv: Option, #[serde(default)] pub export_orientation: Option, - #[serde(default)] - pub style: Option, }