接入Tripo三类3D生成Provider
加入固定 revision 的 tripo-rust-sdk 与 platform-tripo workspace crate 实现 text-to-model、image-to-model、multiview-to-model 提交、通用任务查询和显式模型下载 抽取通用任务类型、状态映射、输出 URL 校验与 SDK 错误归一,保持 wait_for_task 不对外暴露 同步 Provider 边界文档并暂不接入 api-server
This commit is contained in:
+18
@@ -50,6 +50,24 @@ _Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成
|
||||
|
||||
## Language
|
||||
|
||||
### 3D Model Generation
|
||||
|
||||
**3D 模型生成操作**:
|
||||
由文本提示驱动、可异步查询并最终产生模型文件的 provider-neutral 操作;操作状态和产品资源结果分开建模。
|
||||
_Avoid_: 直接把第三方 provider task 当作产品资源、把模型 URL 当作永久资源地址
|
||||
|
||||
**Provider task**:
|
||||
第三方 3D 生成平台返回的任务引用与任务快照,只存在于 provider adapter 的 Rust 边界内;它不是 Genarrative 的内部操作 ID。
|
||||
_Avoid_: 把 Tripo task ID 当作稳定产品 operationId、让 SDK 类型穿透 api-server
|
||||
|
||||
**临时 provider URL**:
|
||||
第三方任务完成后返回的短时模型下载地址,必须通过显式下载流程转换为本地或持久化资源,不能直接当作长期资产地址。
|
||||
_Avoid_: 永久 OSS URL、资源 ID
|
||||
|
||||
**模型 artifact**:
|
||||
由 text-to-model、image-to-model 或 multiview-to-model 任务产生、待应用层登记和持久化的模型文件结果;provider adapter 只提供可下载结果,不负责生成 resourceId 或 assetId。
|
||||
_Avoid_: 仅 UI 中展示的模型链接、没有来源任务的静态文件
|
||||
|
||||
### Puzzle Clear
|
||||
|
||||
**拼消消**:
|
||||
|
||||
+1
@@ -5016,6 +5016,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
## AI 游戏创作与 Agent Runtime
|
||||
|
||||
- [Tripo 3D 模型 Provider 集成](./technical/【技术方案】Tripo文本到3D模型Provider集成-2026-09-18.md):实现 text-to-model、image-to-model、multiview-to-model 的 Rust provider adapter,暂不接入 api-server。
|
||||
|
||||
- [策划 Agent 生产迁移与工作区浏览](./technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md):实施中;以自由协作原型为行为基线,复用生产基建,采用阶段审批与用户工作区文件浏览。无旧 V2 会话的策划入口切换到新设计 Agent。
|
||||
|
||||
- [LLM 累计额度结算](./technical/【技术方案】LLM累计额度结算-2026-09-05.md):Router 累计额度、首次基线与原子钱包结算。
|
||||
@@ -74,6 +76,8 @@
|
||||
|
||||
## 后端、运维与测试
|
||||
|
||||
- [Tripo SDK 隔离与 Provider DTO 边界 ADR](./adr/【ADR】0001-Tripo%20SDK隔离与Provider%20DTO边界-2026-09-18.md):固定 SDK 只存在于 `platform-tripo` 内部,provider task 与产品资源结果分层。
|
||||
|
||||
- [BgFilter 受限资源调度方案](./technical/【后端架构】BgFilter受限资源调度方案-2026-07-21.md)
|
||||
- [Issue225 登录成功 AGC 用户归属修复](./technical/【后端架构】Issue225登录成功AGC用户归属修复方案-2026-09-03.md):登录 route tracking 的真实用户归属、`daily_login` 幂等边界和实施验收。
|
||||
- [SpacetimeDB 连接池取消安全](./【后端架构】SpacetimeDB连接池租约Drop兜底与取消安全-2026-06-11.md)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# 【ADR】0001-Tripo SDK隔离与Provider DTO边界-2026-09-18
|
||||
|
||||
状态:已接受
|
||||
|
||||
`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,不通过裸字符串逃生。
|
||||
@@ -0,0 +1,31 @@
|
||||
# 【技术方案】Tripo文本到3D模型 Provider 集成-2026-09-18
|
||||
|
||||
## 目标
|
||||
|
||||
本阶段在 `server-rs/crates/platform-tripo` 内接入固定 revision 的 `tripo3d-sdk`,完成 text-to-model、image-to-model 和 multiview-to-model 三个 3D 生成入口的 Rust provider adapter。adapter 提供显式配置、提交、单次通用 task 查询和模型下载能力;共享产品契约继续由 `shared-contracts` 管理,并始终生成目录化 ts-rs TypeScript binding。
|
||||
|
||||
代码按 API 目录组织:公共 SDK 配置、client、任务映射、错误、下载类型和通用 task DTO 位于 `platform-tripo/src/common/`;三个生成 API 各自拥有独立目录。Rust 与 TypeScript contracts 按 `common/`、`text_to_model/`、`image_to_model/`、`multiview_to_model/` 分目录生成。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不接入 `api-server`、worker、SpacetimeDB、OSS、计费或 UI。
|
||||
- 不暴露 SDK 的 `wait_for_task`,不在 adapter 内启动后台轮询。
|
||||
- 不实现后处理、rig、animation、图片生成或其它 SDK endpoint。
|
||||
|
||||
## 边界与状态
|
||||
|
||||
`platform-tripo` 是唯一接触 `tripo3d-sdk` 的边界。它返回自己的 provider DTO,不让 SDK 类型穿透到未来的 `api-server`。Tripo 的 `success` 映射为 `completed`;`failed`/`banned` 映射为 `failed`;`cancelled` 和 `expired` 保留为独立终态;未知状态返回结构化错误。
|
||||
|
||||
provider task 结果与产品资源结果分离。provider adapter 不生成 `resourceId` 或 `assetId`;未来应用层在资源持久化后再构造带资源 ID 的产品 DTO。模型 URL 被视为临时 provider 引用,下载由显式方法完成。
|
||||
|
||||
## 请求与类型
|
||||
|
||||
text-to-model 请求包含 SDK 已公开的字段。稳定且已由 SDK 常量定义的字段使用 Rust/TypeScript enum;`compress` 与 `style` 暂时使用带示例 variant 的 enum,并保留官方 API 文档补全 TODO。完整组合校验等待官方 API 文档后一次性落地,不复制不完整的规则集合。
|
||||
|
||||
## 验收
|
||||
|
||||
- `platform-tripo` 不公开 re-export SDK 类型或 `wait_for_task`。
|
||||
- 三个 3D 生成入口的 submit、通用 get task、固定 SDK output 字段映射和显式 download API 可编译。
|
||||
- provider 错误统一为 adapter 错误类型。
|
||||
- shared contracts 的 ts-rs binding 无 feature 开关且始终可生成。
|
||||
- 未运行真实 Provider smoke;需要 API key 的联调另行执行。
|
||||
Generated
+80
-33
@@ -52,17 +52,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
@@ -182,7 +171,7 @@ version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -193,7 +182,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1133,7 +1122,7 @@ dependencies = [
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"socket2",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1148,7 +1137,7 @@ dependencies = [
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1451,7 +1440,7 @@ version = "0.33.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "457481173e6db5ca9fa2be93a58df8f4c7be639587aeb4853b526c6cf87db4e6"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
"document-features",
|
||||
"egui",
|
||||
@@ -1486,7 +1475,7 @@ version = "0.33.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"ahash",
|
||||
"bitflags 2.13.0",
|
||||
"emath",
|
||||
"epaint",
|
||||
@@ -1503,7 +1492,7 @@ version = "0.33.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e4d209971c84b2352a06174abdba701af1e552ce56b144d96f2bd50a3c91236"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
"document-features",
|
||||
"egui",
|
||||
@@ -1598,7 +1587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"ahash 0.8.12",
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
"ecolor",
|
||||
"emath",
|
||||
@@ -1628,7 +1617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2062,9 +2051,6 @@ name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
dependencies = [
|
||||
"ahash 0.7.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
@@ -2094,6 +2080,11 @@ name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -3084,7 +3075,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3739,7 +3730,7 @@ version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527735ac204efb9fa3884bfd9224d016c5735fabe1d394ebed145b40e7545b99"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"ahash",
|
||||
"async-trait",
|
||||
"blake2",
|
||||
"bstr",
|
||||
@@ -3776,7 +3767,7 @@ version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a7ffe2f5acf9f94fd255cfd1438866bc9124f8f0c7d42562bd3f853df2094b7"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"ahash",
|
||||
"async-trait",
|
||||
"brotli",
|
||||
"bstr",
|
||||
@@ -3914,7 +3905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6705a26ad89d241a989a5395641931ba37076f5ab5fbd19ee92402414a43af32"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"hashbrown 0.12.3",
|
||||
"hashbrown 0.17.1",
|
||||
"parking_lot",
|
||||
"rand 0.8.6",
|
||||
]
|
||||
@@ -4164,6 +4155,15 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platform-tripo"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"shared-contracts",
|
||||
"tripo3d-sdk",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platform-wechat"
|
||||
version = "0.1.0"
|
||||
@@ -4856,7 +4856,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5247,6 +5247,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5420,7 +5421,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5529,7 +5530,7 @@ version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c4e9d07e21ad665c45736871e5005d8457ab3d2cdd7806e351aa5e1c8c2ac32"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"ahash",
|
||||
"crossbeam-queue",
|
||||
"either",
|
||||
"hashbrown 0.16.1",
|
||||
@@ -5815,7 +5816,16 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6278,12 +6288,49 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tripo3d-sdk"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/VAST-AI-Research/tripo-rust-sdk.git?rev=1986be88627b04b1fa582a795bf6ac15ae6e0c6d#1986be88627b04b1fa582a795bf6ac15ae6e0c6d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rand 0.8.6",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "12.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"ts-rs-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "12.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
@@ -6901,7 +6948,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7134,7 +7181,7 @@ version = "0.30.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"ahash",
|
||||
"android-activity",
|
||||
"atomic-waker",
|
||||
"bitflags 2.13.0",
|
||||
|
||||
@@ -29,6 +29,7 @@ members = [
|
||||
"crates/platform-matting",
|
||||
"crates/platform-wechat",
|
||||
"crates/platform-speech",
|
||||
"crates/platform-tripo",
|
||||
"crates/platform-editor-agent",
|
||||
"crates/pingora-gateway",
|
||||
"crates/preview-deployer-server",
|
||||
@@ -63,6 +64,7 @@ platform-llm = { path = "crates/platform-llm", default-features = false }
|
||||
platform-matting = { path = "crates/platform-matting", default-features = false }
|
||||
platform-oss = { path = "crates/platform-oss", default-features = false }
|
||||
platform-speech = { path = "crates/platform-speech", default-features = false }
|
||||
platform-tripo = { path = "crates/platform-tripo", default-features = false }
|
||||
platform-wechat = { path = "crates/platform-wechat", default-features = false }
|
||||
pingora-gateway = { path = "crates/pingora-gateway", default-features = false }
|
||||
shared-contracts = { path = "crates/shared-contracts", default-features = false }
|
||||
@@ -114,6 +116,8 @@ time = "0.3"
|
||||
tokio = "1"
|
||||
tokio-stream = "0.1"
|
||||
tokio-tungstenite = "0.27"
|
||||
tripo3d-sdk = { git = "https://github.com/VAST-AI-Research/tripo-rust-sdk.git", rev = "1986be88627b04b1fa582a795bf6ac15ae6e0c6d" }
|
||||
ts-rs = "12.0.1"
|
||||
tower = "0.5"
|
||||
tower-http = "0.6"
|
||||
tracing = "0.1"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "platform-tripo"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
shared-contracts = { workspace = true }
|
||||
tripo3d-sdk = { workspace = true }
|
||||
url = { workspace = true }
|
||||
@@ -0,0 +1,65 @@
|
||||
use tripo3d_sdk::TripoClient;
|
||||
|
||||
use super::{
|
||||
TripoDownloadedModel, TripoError, TripoModelUrl, TripoSettings, TripoTaskHandle,
|
||||
TripoTaskSnapshot, map_task, validate_task_id,
|
||||
};
|
||||
|
||||
pub struct TripoProviderClient {
|
||||
pub(crate) client: TripoClient,
|
||||
}
|
||||
|
||||
impl TripoProviderClient {
|
||||
pub fn new(settings: TripoSettings) -> Result<Self, TripoError> {
|
||||
Ok(Self {
|
||||
client: TripoClient::new(settings.client_options()).map_err(TripoError::from)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_task(
|
||||
&self,
|
||||
handle: &TripoTaskHandle,
|
||||
) -> Result<TripoTaskSnapshot, TripoError> {
|
||||
validate_task_id(&handle.task_id)?;
|
||||
let task = self
|
||||
.client
|
||||
.get_task(&handle.task_id)
|
||||
.await
|
||||
.map_err(TripoError::from)?;
|
||||
map_task(task)
|
||||
}
|
||||
|
||||
pub async fn download_model(
|
||||
&self,
|
||||
task: &TripoTaskSnapshot,
|
||||
) -> Result<TripoDownloadedModel, TripoError> {
|
||||
let handle = &task.handle;
|
||||
validate_task_id(&handle.task_id)?;
|
||||
let sdk_task = self
|
||||
.client
|
||||
.get_task(&handle.task_id)
|
||||
.await
|
||||
.map_err(TripoError::from)?;
|
||||
let downloaded = self
|
||||
.client
|
||||
.download_model(&sdk_task)
|
||||
.await
|
||||
.map_err(TripoError::from)?
|
||||
.ok_or_else(|| TripoError::OutputSchema {
|
||||
task_id: handle.task_id.clone(),
|
||||
message: "completed task has no model URL".into(),
|
||||
})?;
|
||||
let url = TripoModelUrl::parse(&downloaded.url).map_err(|error| match error {
|
||||
TripoError::OutputSchema { message, .. } => TripoError::OutputSchema {
|
||||
task_id: handle.task_id.clone(),
|
||||
message,
|
||||
},
|
||||
other => other,
|
||||
})?;
|
||||
Ok(TripoDownloadedModel {
|
||||
url,
|
||||
content_type: downloaded.content_type,
|
||||
data: downloaded.data,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use tripo3d_sdk::ClientOptions;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TripoSettings {
|
||||
pub api_key: String,
|
||||
pub base_url: String,
|
||||
pub request_timeout: Duration,
|
||||
pub retries: u32,
|
||||
pub user_agent: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TripoSettings {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("TripoSettings")
|
||||
.field("api_key", &"<redacted>")
|
||||
.field("base_url", &self.base_url)
|
||||
.field("request_timeout", &self.request_timeout)
|
||||
.field("retries", &self.retries)
|
||||
.field("user_agent", &self.user_agent)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TripoSettings {
|
||||
pub fn new(
|
||||
api_key: String,
|
||||
base_url: String,
|
||||
request_timeout: Duration,
|
||||
retries: u32,
|
||||
user_agent: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
api_key,
|
||||
base_url,
|
||||
request_timeout,
|
||||
retries,
|
||||
user_agent,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn client_options(&self) -> ClientOptions {
|
||||
ClientOptions {
|
||||
api_key: Some(self.api_key.clone()),
|
||||
base_url: Some(self.base_url.clone()),
|
||||
timeout: Some(self.request_timeout),
|
||||
retries: Some(self.retries),
|
||||
user_agent: Some(self.user_agent.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TripoField {
|
||||
Prompt,
|
||||
Model,
|
||||
NegativePrompt,
|
||||
ImageSeed,
|
||||
ModelSeed,
|
||||
TextureSeed,
|
||||
Texture,
|
||||
Pbr,
|
||||
TextureQuality,
|
||||
GeometryQuality,
|
||||
FaceLimit,
|
||||
AutoSize,
|
||||
Quad,
|
||||
SmartLowPoly,
|
||||
GenerateParts,
|
||||
Compress,
|
||||
ExportUv,
|
||||
ExportOrientation,
|
||||
Style,
|
||||
TaskId,
|
||||
}
|
||||
|
||||
impl fmt::Display for TripoField {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let name = match self {
|
||||
Self::Prompt => "prompt",
|
||||
Self::Model => "model",
|
||||
Self::NegativePrompt => "negative_prompt",
|
||||
Self::ImageSeed => "image_seed",
|
||||
Self::ModelSeed => "model_seed",
|
||||
Self::TextureSeed => "texture_seed",
|
||||
Self::Texture => "texture",
|
||||
Self::Pbr => "pbr",
|
||||
Self::TextureQuality => "texture_quality",
|
||||
Self::GeometryQuality => "geometry_quality",
|
||||
Self::FaceLimit => "face_limit",
|
||||
Self::AutoSize => "auto_size",
|
||||
Self::Quad => "quad",
|
||||
Self::SmartLowPoly => "smart_low_poly",
|
||||
Self::GenerateParts => "generate_parts",
|
||||
Self::Compress => "compress",
|
||||
Self::ExportUv => "export_uv",
|
||||
Self::ExportOrientation => "export_orientation",
|
||||
Self::Style => "style",
|
||||
Self::TaskId => "task_id",
|
||||
};
|
||||
f.write_str(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TripoValidationReason {
|
||||
Required,
|
||||
InvalidCombination,
|
||||
}
|
||||
|
||||
impl fmt::Display for TripoValidationReason {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Required => "required",
|
||||
Self::InvalidCombination => "invalid combination",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TripoError {
|
||||
InvalidParameters {
|
||||
field: Option<TripoField>,
|
||||
reason: TripoValidationReason,
|
||||
message: String,
|
||||
},
|
||||
SdkInvalidArgument(String),
|
||||
Api {
|
||||
code: i64,
|
||||
message: Option<String>,
|
||||
suggestion: Option<String>,
|
||||
status: Option<u16>,
|
||||
},
|
||||
Request {
|
||||
message: String,
|
||||
status: Option<u16>,
|
||||
},
|
||||
TaskFailure {
|
||||
task_id: String,
|
||||
status: String,
|
||||
message: Option<String>,
|
||||
},
|
||||
OutputSchema {
|
||||
task_id: String,
|
||||
message: String,
|
||||
},
|
||||
Sdk {
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for TripoError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidParameters {
|
||||
field,
|
||||
reason,
|
||||
message,
|
||||
} => {
|
||||
write!(f, "invalid Tripo parameters ({reason})")?;
|
||||
match field {
|
||||
Some(field) => write!(f, " [{field}]: {message}"),
|
||||
None => write!(f, ": {message}"),
|
||||
}
|
||||
}
|
||||
Self::SdkInvalidArgument(message) => write!(f, "Tripo SDK invalid argument: {message}"),
|
||||
Self::Api {
|
||||
code,
|
||||
message,
|
||||
suggestion,
|
||||
status,
|
||||
} => write!(
|
||||
f,
|
||||
"Tripo API error code={code} status={status:?} message={message:?} suggestion={suggestion:?}"
|
||||
),
|
||||
Self::Request { message, status } => {
|
||||
write!(f, "Tripo request error status={status:?}: {message}")
|
||||
}
|
||||
Self::TaskFailure {
|
||||
task_id,
|
||||
status,
|
||||
message,
|
||||
} => write!(f, "Tripo task {task_id} ended with {status}: {message:?}"),
|
||||
Self::OutputSchema { task_id, message } => {
|
||||
write!(f, "Tripo task {task_id} output schema error: {message}")
|
||||
}
|
||||
Self::Sdk { message } => write!(f, "Tripo SDK error: {message}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TripoError {}
|
||||
|
||||
impl TripoError {
|
||||
pub fn is_retryable(&self) -> bool {
|
||||
match self {
|
||||
Self::Api { status, .. } => matches!(status, Some(429 | 500..=599)),
|
||||
Self::Request { status, .. } => {
|
||||
status.is_none() || matches!(status, Some(408 | 425 | 429 | 500..=599))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tripo3d_sdk::Error> for TripoError {
|
||||
fn from(error: tripo3d_sdk::Error) -> Self {
|
||||
match error {
|
||||
tripo3d_sdk::Error::InvalidArgument(message) => Self::SdkInvalidArgument(message),
|
||||
tripo3d_sdk::Error::Api {
|
||||
code,
|
||||
message,
|
||||
suggestion,
|
||||
status,
|
||||
} => Self::Api {
|
||||
code,
|
||||
message,
|
||||
suggestion,
|
||||
status,
|
||||
},
|
||||
tripo3d_sdk::Error::Request {
|
||||
message, status, ..
|
||||
} => Self::Request { message, status },
|
||||
tripo3d_sdk::Error::Task { task } => Self::TaskFailure {
|
||||
task_id: task.task_id.clone(),
|
||||
status: task.status.to_string(),
|
||||
message: task.error_message.clone(),
|
||||
},
|
||||
tripo3d_sdk::Error::Timeout {
|
||||
task_id,
|
||||
timeout_ms,
|
||||
} => Self::Request {
|
||||
message: format!("unexpected SDK timeout after {timeout_ms}ms for task {task_id}"),
|
||||
status: None,
|
||||
},
|
||||
tripo3d_sdk::Error::Io(error) => Self::Sdk {
|
||||
message: error.to_string(),
|
||||
},
|
||||
tripo3d_sdk::Error::Serde(error) => Self::Sdk {
|
||||
message: error.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use shared_contracts::model3d::common::Model3dTaskStatus;
|
||||
use tripo3d_sdk::{Task, TaskOutput, TaskStatus};
|
||||
|
||||
use super::{
|
||||
TripoError, TripoModelUrl, TripoTaskFailure, TripoTaskHandle, TripoTaskOutput,
|
||||
TripoTaskSnapshot, TripoTaskType,
|
||||
};
|
||||
|
||||
pub(crate) fn map_task(task: Task) -> Result<TripoTaskSnapshot, TripoError> {
|
||||
let task_id = task.task_id.clone();
|
||||
let task_type = map_task_type(&task.task_type, &task_id)?;
|
||||
let status = map_status(task.status)?;
|
||||
let output = match status {
|
||||
Model3dTaskStatus::Completed => Some(map_output(
|
||||
&task_id,
|
||||
task.output
|
||||
.as_ref()
|
||||
.ok_or_else(|| TripoError::OutputSchema {
|
||||
task_id: task_id.clone(),
|
||||
message: "completed task has no output".into(),
|
||||
})?,
|
||||
)?),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(TripoTaskSnapshot {
|
||||
handle: TripoTaskHandle { task_id },
|
||||
task_type,
|
||||
status,
|
||||
progress: task.progress,
|
||||
output,
|
||||
failure: (task.error_code.is_some() || task.error_message.is_some()).then(|| {
|
||||
TripoTaskFailure {
|
||||
code: task.error_code,
|
||||
message: task.error_message.clone(),
|
||||
}
|
||||
}),
|
||||
created_at: task.created_at,
|
||||
completed_at: task.completed_at,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_task_type(task_type: &str, task_id: &str) -> Result<TripoTaskType, TripoError> {
|
||||
match task_type {
|
||||
"text_to_model" => Ok(TripoTaskType::TextToModel),
|
||||
"image_to_model" => Ok(TripoTaskType::ImageToModel),
|
||||
"multiview_to_model" => Ok(TripoTaskType::MultiviewToModel),
|
||||
other => Err(TripoError::OutputSchema {
|
||||
task_id: task_id.into(),
|
||||
message: format!("unsupported task type: {other}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_status(status: TaskStatus) -> Result<Model3dTaskStatus, TripoError> {
|
||||
match status {
|
||||
TaskStatus::Queued => Ok(Model3dTaskStatus::Queued),
|
||||
TaskStatus::Running => Ok(Model3dTaskStatus::Running),
|
||||
TaskStatus::Success => Ok(Model3dTaskStatus::Completed),
|
||||
TaskStatus::Failed | TaskStatus::Banned => Ok(Model3dTaskStatus::Failed),
|
||||
TaskStatus::Cancelled => Ok(Model3dTaskStatus::Cancelled),
|
||||
TaskStatus::Expired => Ok(Model3dTaskStatus::Expired),
|
||||
TaskStatus::Unknown => Err(TripoError::Sdk {
|
||||
message: "Tripo returned unknown task status".into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_output(task_id: &str, output: &TaskOutput) -> Result<TripoTaskOutput, TripoError> {
|
||||
let mut model_urls = Vec::new();
|
||||
for candidate in output
|
||||
.model_urls
|
||||
.iter()
|
||||
.flat_map(|urls| urls.iter())
|
||||
.chain(output.model_url.iter())
|
||||
.chain(output.model.iter())
|
||||
.chain(output.pbr_model.iter())
|
||||
.chain(output.base_model.iter())
|
||||
{
|
||||
if !model_urls
|
||||
.iter()
|
||||
.any(|url: &TripoModelUrl| url.as_str() == candidate)
|
||||
{
|
||||
model_urls.push(parse_url(candidate, task_id)?);
|
||||
}
|
||||
}
|
||||
if model_urls.is_empty() {
|
||||
return Err(TripoError::OutputSchema {
|
||||
task_id: task_id.into(),
|
||||
message: "completed task output contains no model URL".into(),
|
||||
});
|
||||
}
|
||||
|
||||
let rendered_image_url = output
|
||||
.rendered_image_url
|
||||
.as_ref()
|
||||
.or(output.rendered_image.as_ref())
|
||||
.map(|value| parse_url(value, task_id))
|
||||
.transpose()?;
|
||||
let generated_image_url = output
|
||||
.generated_image_url
|
||||
.as_ref()
|
||||
.map(|value| parse_url(value, task_id))
|
||||
.transpose()?;
|
||||
|
||||
Ok(TripoTaskOutput {
|
||||
model_urls,
|
||||
rendered_image_url,
|
||||
generated_image_url,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_url(value: &str, task_id: &str) -> Result<TripoModelUrl, TripoError> {
|
||||
TripoModelUrl::parse(value).map_err(|error| match error {
|
||||
TripoError::OutputSchema { message, .. } => TripoError::OutputSchema {
|
||||
task_id: task_id.into(),
|
||||
message,
|
||||
},
|
||||
other => other,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
mod client;
|
||||
mod config;
|
||||
mod error;
|
||||
mod mapping;
|
||||
mod types;
|
||||
mod validation;
|
||||
|
||||
pub use client::TripoProviderClient;
|
||||
pub use config::TripoSettings;
|
||||
pub use error::{TripoError, TripoField, TripoValidationReason};
|
||||
pub(crate) use mapping::map_task;
|
||||
pub use types::{
|
||||
TripoDownloadedModel, TripoModelUrl, TripoTaskFailure, TripoTaskHandle, TripoTaskOutput,
|
||||
TripoTaskSnapshot, TripoTaskType,
|
||||
};
|
||||
pub(crate) use validation::validate_task_id;
|
||||
@@ -0,0 +1,97 @@
|
||||
use std::fmt;
|
||||
|
||||
use url::Url;
|
||||
|
||||
use shared_contracts::model3d::common::Model3dTaskStatus;
|
||||
|
||||
use crate::common::TripoError;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TripoTaskHandle {
|
||||
pub task_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum TripoTaskType {
|
||||
TextToModel,
|
||||
ImageToModel,
|
||||
MultiviewToModel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TripoModelUrl(Url);
|
||||
|
||||
impl TripoModelUrl {
|
||||
pub(crate) fn parse(value: &str) -> Result<Self, TripoError> {
|
||||
let url = Url::parse(value).map_err(|error| TripoError::OutputSchema {
|
||||
task_id: "unknown".into(),
|
||||
message: format!("model URL is invalid: {error}"),
|
||||
})?;
|
||||
if !matches!(url.scheme(), "http" | "https") {
|
||||
return Err(TripoError::OutputSchema {
|
||||
task_id: "unknown".into(),
|
||||
message: "model URL must use http or https".into(),
|
||||
});
|
||||
}
|
||||
Ok(Self(url))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TripoModelUrl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TripoTaskFailure {
|
||||
pub code: Option<i64>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TripoTaskOutput {
|
||||
pub model_urls: Vec<TripoModelUrl>,
|
||||
pub rendered_image_url: Option<TripoModelUrl>,
|
||||
pub generated_image_url: Option<TripoModelUrl>,
|
||||
// TODO(tripo-docs): add fixed per-task output fields after the provider
|
||||
// contract is supplied and verified against real task responses.
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TripoTaskSnapshot {
|
||||
pub handle: TripoTaskHandle,
|
||||
pub task_type: TripoTaskType,
|
||||
pub status: Model3dTaskStatus,
|
||||
pub progress: Option<u32>,
|
||||
pub output: Option<TripoTaskOutput>,
|
||||
pub failure: Option<TripoTaskFailure>,
|
||||
pub created_at: Option<String>,
|
||||
pub completed_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TripoDownloadedModel {
|
||||
pub url: TripoModelUrl,
|
||||
pub content_type: Option<String>,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TripoDownloadedModel {
|
||||
pub fn filename(&self, name: &str) -> String {
|
||||
let extension = self
|
||||
.url
|
||||
.0
|
||||
.path_segments()
|
||||
.and_then(|segments| segments.last())
|
||||
.and_then(|segment| segment.rsplit_once('.'))
|
||||
.map(|(_, extension)| extension)
|
||||
.filter(|extension| !extension.is_empty())
|
||||
.unwrap_or("glb");
|
||||
format!("{name}.{extension}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use super::{TripoError, TripoField, TripoValidationReason};
|
||||
|
||||
pub(crate) fn validate_task_id(task_id: &str) -> Result<(), TripoError> {
|
||||
if task_id.trim().is_empty() {
|
||||
return Err(TripoError::InvalidParameters {
|
||||
field: Some(TripoField::TaskId),
|
||||
reason: TripoValidationReason::Required,
|
||||
message: "task_id must not be blank".into(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use shared_contracts::model3d::common::{
|
||||
Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dInputOrientation,
|
||||
Model3dModelVersion, Model3dStyle, Model3dTextureAlignment, Model3dTextureQuality,
|
||||
};
|
||||
use shared_contracts::model3d::image_to_model::Model3dImageToModelRequest;
|
||||
use tripo3d_sdk::{models::FileInput, params::ImageToModelParams};
|
||||
|
||||
use crate::common::{TripoError, TripoProviderClient, TripoTaskHandle};
|
||||
|
||||
impl TripoProviderClient {
|
||||
pub async fn submit_image_to_model(
|
||||
&self,
|
||||
request: &Model3dImageToModelRequest,
|
||||
) -> Result<TripoTaskHandle, TripoError> {
|
||||
if request.input.trim().is_empty() {
|
||||
return Err(TripoError::SdkInvalidArgument(
|
||||
"image_to_model: input is required".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let task_id = self
|
||||
.client
|
||||
.image_to_model(to_sdk_params(request))
|
||||
.await
|
||||
.map_err(TripoError::from)?;
|
||||
Ok(TripoTaskHandle { task_id })
|
||||
}
|
||||
}
|
||||
|
||||
fn to_sdk_params(request: &Model3dImageToModelRequest) -> ImageToModelParams {
|
||||
ImageToModelParams {
|
||||
input: FileInput::from(request.input.as_str()),
|
||||
model: request
|
||||
.model
|
||||
.map(Model3dModelVersion::as_str)
|
||||
.map(str::to_owned),
|
||||
enable_image_autofix: request.enable_image_autofix,
|
||||
model_seed: request.model_seed,
|
||||
texture_seed: request.texture_seed,
|
||||
texture: request.texture,
|
||||
pbr: request.pbr,
|
||||
texture_quality: request
|
||||
.texture_quality
|
||||
.map(Model3dTextureQuality::as_str)
|
||||
.map(str::to_owned),
|
||||
texture_alignment: request
|
||||
.texture_alignment
|
||||
.map(Model3dTextureAlignment::as_str)
|
||||
.map(str::to_owned),
|
||||
geometry_quality: request
|
||||
.geometry_quality
|
||||
.map(Model3dGeometryQuality::as_str)
|
||||
.map(str::to_owned),
|
||||
face_limit: request.face_limit,
|
||||
auto_size: request.auto_size,
|
||||
orientation: request
|
||||
.orientation
|
||||
.map(Model3dInputOrientation::as_str)
|
||||
.map(str::to_owned),
|
||||
quad: request.quad,
|
||||
smart_low_poly: request.smart_low_poly,
|
||||
generate_parts: request.generate_parts,
|
||||
compress: request
|
||||
.compress
|
||||
.map(Model3dCompression::as_str)
|
||||
.map(str::to_owned),
|
||||
export_uv: request.export_uv,
|
||||
export_orientation: request
|
||||
.export_orientation
|
||||
.map(Model3dExportOrientation::as_str)
|
||||
.map(str::to_owned),
|
||||
style: request.style.map(Model3dStyle::as_str).map(str::to_owned),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
mod client;
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Tripo text-to-model provider adapter.
|
||||
//!
|
||||
//! The SDK is deliberately private to this crate. Callers receive provider
|
||||
//! DTOs and `TripoError`, never `tripo3d_sdk` types or its poller API.
|
||||
|
||||
mod common;
|
||||
mod image_to_model;
|
||||
mod multiview_to_model;
|
||||
mod text_to_model;
|
||||
|
||||
pub use common::{
|
||||
TripoDownloadedModel, TripoError, TripoField, TripoModelUrl, TripoProviderClient,
|
||||
TripoSettings, TripoTaskFailure, TripoTaskHandle, TripoTaskOutput, TripoTaskSnapshot,
|
||||
TripoTaskType, TripoValidationReason,
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
use shared_contracts::model3d::common::{
|
||||
Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dInputOrientation,
|
||||
Model3dModelVersion, Model3dTextureAlignment, Model3dTextureQuality,
|
||||
};
|
||||
use shared_contracts::model3d::multiview_to_model::{
|
||||
Model3dMultiviewInputs, Model3dMultiviewToModelRequest,
|
||||
};
|
||||
use tripo3d_sdk::{models::FileInput, params::MultiviewToModelParams};
|
||||
|
||||
use crate::common::{TripoError, TripoProviderClient, TripoTaskHandle};
|
||||
|
||||
impl TripoProviderClient {
|
||||
pub async fn submit_multiview_to_model(
|
||||
&self,
|
||||
request: &Model3dMultiviewToModelRequest,
|
||||
) -> Result<TripoTaskHandle, TripoError> {
|
||||
let task_id = self
|
||||
.client
|
||||
.multiview_to_model(to_sdk_params(request))
|
||||
.await
|
||||
.map_err(TripoError::from)?;
|
||||
Ok(TripoTaskHandle { task_id })
|
||||
}
|
||||
}
|
||||
|
||||
fn to_sdk_params(request: &Model3dMultiviewToModelRequest) -> MultiviewToModelParams {
|
||||
let mut params = match &request.inputs {
|
||||
Model3dMultiviewInputs::Views {
|
||||
front,
|
||||
left,
|
||||
back,
|
||||
right,
|
||||
} => MultiviewToModelParams::from_views([
|
||||
Some(FileInput::from(front.as_str())),
|
||||
left.as_deref().map(FileInput::from),
|
||||
back.as_deref().map(FileInput::from),
|
||||
right.as_deref().map(FileInput::from),
|
||||
]),
|
||||
Model3dMultiviewInputs::TaskId { task_id } => {
|
||||
MultiviewToModelParams::from_task_id(task_id.clone())
|
||||
}
|
||||
};
|
||||
params.model = request
|
||||
.model
|
||||
.map(Model3dModelVersion::as_str)
|
||||
.map(str::to_owned);
|
||||
params.model_seed = request.model_seed;
|
||||
params.texture_seed = request.texture_seed;
|
||||
params.texture = request.texture;
|
||||
params.pbr = request.pbr;
|
||||
params.texture_quality = request
|
||||
.texture_quality
|
||||
.map(Model3dTextureQuality::as_str)
|
||||
.map(str::to_owned);
|
||||
params.geometry_quality = request
|
||||
.geometry_quality
|
||||
.map(Model3dGeometryQuality::as_str)
|
||||
.map(str::to_owned);
|
||||
params.texture_alignment = request
|
||||
.texture_alignment
|
||||
.map(Model3dTextureAlignment::as_str)
|
||||
.map(str::to_owned);
|
||||
params.face_limit = request.face_limit;
|
||||
params.auto_size = request.auto_size;
|
||||
params.orientation = request
|
||||
.orientation
|
||||
.map(Model3dInputOrientation::as_str)
|
||||
.map(str::to_owned);
|
||||
params.quad = request.quad;
|
||||
params.smart_low_poly = request.smart_low_poly;
|
||||
params.generate_parts = request.generate_parts;
|
||||
params.compress = request
|
||||
.compress
|
||||
.map(Model3dCompression::as_str)
|
||||
.map(str::to_owned);
|
||||
params.export_uv = request.export_uv;
|
||||
params.export_orientation = request
|
||||
.export_orientation
|
||||
.map(Model3dExportOrientation::as_str)
|
||||
.map(str::to_owned);
|
||||
params
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
mod client;
|
||||
@@ -0,0 +1,63 @@
|
||||
use crate::common::{TripoError, TripoProviderClient, TripoTaskHandle};
|
||||
use shared_contracts::model3d::common::{
|
||||
Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dModelVersion,
|
||||
Model3dStyle, Model3dTextureQuality,
|
||||
};
|
||||
use shared_contracts::model3d::text_to_model::Model3dTextToModelRequest;
|
||||
|
||||
use super::validation::validate_text_to_model_request;
|
||||
|
||||
impl TripoProviderClient {
|
||||
pub async fn submit(
|
||||
&self,
|
||||
request: &Model3dTextToModelRequest,
|
||||
) -> Result<TripoTaskHandle, TripoError> {
|
||||
validate_text_to_model_request(request)?;
|
||||
let task_id = self
|
||||
.client
|
||||
.text_to_model(to_sdk_params(request))
|
||||
.await
|
||||
.map_err(TripoError::from)?;
|
||||
Ok(TripoTaskHandle { task_id })
|
||||
}
|
||||
}
|
||||
|
||||
fn to_sdk_params(request: &Model3dTextToModelRequest) -> tripo3d_sdk::params::TextToModelParams {
|
||||
tripo3d_sdk::params::TextToModelParams {
|
||||
prompt: request.prompt.clone(),
|
||||
model: request
|
||||
.model
|
||||
.map(Model3dModelVersion::as_str)
|
||||
.map(str::to_owned),
|
||||
negative_prompt: request.negative_prompt.clone(),
|
||||
image_seed: request.image_seed,
|
||||
model_seed: request.model_seed,
|
||||
texture_seed: request.texture_seed,
|
||||
texture: request.texture,
|
||||
pbr: request.pbr,
|
||||
texture_quality: request
|
||||
.texture_quality
|
||||
.map(Model3dTextureQuality::as_str)
|
||||
.map(str::to_owned),
|
||||
geometry_quality: request
|
||||
.geometry_quality
|
||||
.map(Model3dGeometryQuality::as_str)
|
||||
.map(str::to_owned),
|
||||
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
|
||||
.map(Model3dCompression::as_str)
|
||||
.map(str::to_owned),
|
||||
export_uv: request.export_uv,
|
||||
export_orientation: request
|
||||
.export_orientation
|
||||
.map(Model3dExportOrientation::as_str)
|
||||
.map(str::to_owned),
|
||||
style: request.style.map(Model3dStyle::as_str).map(str::to_owned),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mod client;
|
||||
mod validation;
|
||||
@@ -0,0 +1,19 @@
|
||||
use shared_contracts::model3d::text_to_model::Model3dTextToModelRequest;
|
||||
|
||||
use crate::common::{TripoError, TripoField, TripoValidationReason};
|
||||
|
||||
pub(crate) fn validate_text_to_model_request(
|
||||
request: &Model3dTextToModelRequest,
|
||||
) -> Result<(), TripoError> {
|
||||
if request.prompt.trim().is_empty() {
|
||||
return Err(TripoError::InvalidParameters {
|
||||
field: Some(TripoField::Prompt),
|
||||
reason: TripoValidationReason::Required,
|
||||
message: "prompt must not be blank".into(),
|
||||
});
|
||||
}
|
||||
|
||||
// TODO(tripo-docs): add the complete documented cross-field validation
|
||||
// matrix in one pass when the official text-to-model API contract arrives.
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user