Files
Genarrative/server-rs/crates/platform-tripo/src/common/validation.rs
T
k88936 2e9aed3750 三个提交入口统一校验 provider 返回的 task id
- 新增 submitted_task_handle:空白 id 在提交调用附近以 OutputSchema 失败
- text / image / multiview 提交路径共用它,任务不再以孤儿状态等到下游查询才报错
2026-09-22 15:53:37 +08:00

281 lines
9.3 KiB
Rust

use shared_contracts::model3d::common::{
Model3dCompression, Model3dGeometryQuality, Model3dModelVersion, Model3dTextureQuality,
Model3dTextureVersion,
};
use shared_contracts::model3d::image_to_model::Model3dImageToModelParams;
use shared_contracts::model3d::text_to_model::Model3dTextToModelParams;
use super::{TripoError, TripoField, TripoTaskHandle, TripoValidationReason};
pub(crate) struct TripoGenerationOptions {
pub model: Model3dModelVersion,
pub texture: Option<bool>,
pub pbr: Option<bool>,
pub texture_quality: Option<Model3dTextureQuality>,
pub texture_version: Option<Model3dTextureVersion>,
pub geometry_quality: Option<Model3dGeometryQuality>,
pub face_limit: Option<i64>,
pub auto_size: Option<bool>,
pub quad: Option<bool>,
pub smart_low_poly: Option<bool>,
pub generate_parts: Option<bool>,
pub compress: Option<Model3dCompression>,
}
impl From<&Model3dTextToModelParams> for TripoGenerationOptions {
fn from(params: &Model3dTextToModelParams) -> Self {
Self {
model: params.model,
texture: params.texture,
pbr: params.pbr,
texture_quality: params.texture_quality,
texture_version: params.texture_version,
geometry_quality: params.geometry_quality,
face_limit: params.face_limit,
auto_size: params.auto_size,
quad: params.quad,
smart_low_poly: params.smart_low_poly,
generate_parts: params.generate_parts,
compress: params.compress,
}
}
}
impl From<&Model3dImageToModelParams> for TripoGenerationOptions {
fn from(params: &Model3dImageToModelParams) -> Self {
Self {
model: params.model,
texture: params.texture,
pbr: params.pbr,
texture_quality: params.texture_quality,
texture_version: params.texture_version,
geometry_quality: params.geometry_quality,
face_limit: params.face_limit,
auto_size: params.auto_size,
quad: params.quad,
smart_low_poly: params.smart_low_poly,
generate_parts: params.generate_parts,
compress: params.compress,
}
}
}
pub(crate) fn validate_generation_options(
options: &TripoGenerationOptions,
) -> Result<(), TripoError> {
let family = ModelFamily::from(options.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 == Some(true) && 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::OutOfRange,
&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<Model3dModelVersion> 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<TripoField>, 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 {
field: Some(TripoField::TaskId),
reason: TripoValidationReason::Required,
message: "task_id must not be blank".into(),
});
}
Ok(())
}
/// 提交成功后立刻收口 provider 返回的 task id。
///
/// 空白 id 是 provider 违约,不是调用方参数问题:必须在提交调用附近以 `OutputSchema`
/// 失败,而不是拖到下游 `get_task` 才报错——那时任务已经提交出去,成为无法追踪的孤儿。
pub(crate) fn submitted_task_handle(task_id: String) -> Result<TripoTaskHandle, TripoError> {
let task_id = task_id.trim().to_string();
if task_id.is_empty() {
return Err(TripoError::OutputSchema {
task_id: "submit".into(),
message: "submit response has no task_id".into(),
});
}
Ok(TripoTaskHandle { task_id })
}