分层Tripo生成的provider参数与平台API请求契约
- common 新增 Model3dGenerationSource、Model3dGenerationTarget、Model3dGenerationTargetRef 与产物元数据 - text/image 各自的参数类型独立成 params,API 请求为 generation + 平台字段 + target - 图片输入只接受站内引用,input 由服务端解析后注入 provider - 删除宽松的作业与结果占位 DTO,改为按端点的严格结果与端点 tagged enum - 新增契约严格性测试,覆盖分支必填字段、图片输入歧义与顶层未知字段
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::MODEL3D_TS_EXPORT_DIR;
|
||||
|
||||
/// 单个生成产物的对象元数据。
|
||||
///
|
||||
/// 只描述服务端已经持久化的正式对象;不带 provider task ID,也不带带签名的临时下载地址。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub struct Model3dGeneratedArtifact {
|
||||
pub object_key: String,
|
||||
pub content_type: String,
|
||||
/// 对象字节数;单体模型产物为几十 MB,远低于 2^53,按 TS number 导出。
|
||||
#[ts(type = "number")]
|
||||
pub content_length: u64,
|
||||
pub sha256: String,
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::MODEL3D_TS_EXPORT_DIR;
|
||||
|
||||
/// API 层图片输入:只接受站内的画布资源或素材库对象,二者互斥。
|
||||
///
|
||||
/// 不接受裸字符串、远程 URL 与 data URL,因此 provider 需要的图片地址由服务端
|
||||
/// 按分支校验归属后解析,客户端无法绕过归属校验直接给 provider 传地址。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
// 注:该属性由 serde 生效,变体里的未知字段(例如同时给出两个 ID)会被拒绝;
|
||||
// 已发布版 ts-rs 无法在 TS 绑定里表达这份严格性,见 model3d_multiview_request_contract.rs 的同类说明。
|
||||
#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub enum Model3dGenerationSource {
|
||||
Resource {
|
||||
#[serde(rename = "resourceId")]
|
||||
resource_id: String,
|
||||
},
|
||||
Asset {
|
||||
#[serde(rename = "assetId")]
|
||||
asset_id: String,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::MODEL3D_TS_EXPORT_DIR;
|
||||
use crate::editor_canvas::EditorCanvasGenerationCompletionPayload;
|
||||
|
||||
/// 生成结果的落点。两个分支都必须给出定位字段,不存在“都不给就默认落素材库”的口径。
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub enum Model3dGenerationTarget {
|
||||
ProjectResource {
|
||||
#[serde(rename = "projectId")]
|
||||
project_id: String,
|
||||
/// 画布占位框回填载荷:结果落库后前端据此把新资源放到用户提交时的位置。
|
||||
#[serde(
|
||||
rename = "canvasCompletion",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
canvas_completion: Option<EditorCanvasGenerationCompletionPayload>,
|
||||
},
|
||||
AssetLibrary {
|
||||
#[serde(rename = "folderId")]
|
||||
folder_id: String,
|
||||
label: String,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::MODEL3D_TS_EXPORT_DIR;
|
||||
|
||||
/// 结果落点引用:与请求 `target` 同分支,但给的是已创建资源 / 素材的正式 ID。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub enum Model3dGenerationTargetRef {
|
||||
ProjectResource {
|
||||
#[serde(rename = "resourceId")]
|
||||
resource_id: String,
|
||||
},
|
||||
AssetLibrary {
|
||||
#[serde(rename = "assetId")]
|
||||
asset_id: String,
|
||||
},
|
||||
}
|
||||
@@ -2,6 +2,10 @@ pub(crate) const MODEL3D_TS_EXPORT_DIR: &str = crate::model3d::model3d_ts_export
|
||||
|
||||
mod compression;
|
||||
mod export_orientation;
|
||||
mod generated_artifact;
|
||||
mod generation_source;
|
||||
mod generation_target;
|
||||
mod generation_target_ref;
|
||||
mod geometry_quality;
|
||||
mod input_orientation;
|
||||
mod model_version;
|
||||
@@ -13,6 +17,10 @@ mod texture_version;
|
||||
|
||||
pub use compression::Model3dCompression;
|
||||
pub use export_orientation::Model3dExportOrientation;
|
||||
pub use generated_artifact::Model3dGeneratedArtifact;
|
||||
pub use generation_source::Model3dGenerationSource;
|
||||
pub use generation_target::Model3dGenerationTarget;
|
||||
pub use generation_target_ref::Model3dGenerationTargetRef;
|
||||
pub use geometry_quality::Model3dGeometryQuality;
|
||||
pub use input_orientation::Model3dInputOrientation;
|
||||
pub use model_version::Model3dModelVersion;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::image_to_model::Model3dImageToModelResult;
|
||||
use super::text_to_model::Model3dTextToModelResult;
|
||||
|
||||
pub(crate) const MODEL3D_TS_EXPORT_DIR: &str = crate::model3d::model3d_ts_export_dir!("");
|
||||
|
||||
/// 完成结果按端点严格 tagged enum:每个分支只携带该端点真正会产出的字段,
|
||||
/// 不使用“所有端点共用一份大结构 + 一堆 Option”的宽松形态。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub enum Model3dGenerationResult {
|
||||
TextToModel(Model3dTextToModelResult),
|
||||
ImageToModel(Model3dImageToModelResult),
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
pub(crate) const MODEL3D_TS_EXPORT_DIR: &str =
|
||||
crate::model3d::model3d_ts_export_dir!("image-to-model/");
|
||||
|
||||
mod params;
|
||||
mod request;
|
||||
mod result;
|
||||
|
||||
pub use params::Model3dImageToModelParams;
|
||||
pub use request::Model3dImageToModelRequest;
|
||||
pub use result::Model3dImageToModelResult;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model3d::common::{
|
||||
Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dInputOrientation,
|
||||
Model3dModelVersion, Model3dTextureAlignment, Model3dTextureQuality, Model3dTextureVersion,
|
||||
};
|
||||
|
||||
use super::MODEL3D_TS_EXPORT_DIR;
|
||||
|
||||
/// image-to-model 的 provider 生成参数。
|
||||
///
|
||||
/// 图片本体不在 provider 参数里:客户端只能给 `Model3dGenerationSource`,
|
||||
/// 服务端解析归属后把 provider 需要的地址注入调用,见 `api-server::tripo3d`。
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub struct Model3dImageToModelParams {
|
||||
pub model: Model3dModelVersion,
|
||||
#[ts(optional = nullable)]
|
||||
pub enable_image_autofix: Option<bool>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub model_seed: Option<i64>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_seed: Option<i64>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub pbr: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_quality: Option<Model3dTextureQuality>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_version: Option<Model3dTextureVersion>,
|
||||
#[ts(optional = nullable)]
|
||||
pub delight: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_alignment: Option<Model3dTextureAlignment>,
|
||||
#[ts(optional = nullable)]
|
||||
pub geometry_quality: Option<Model3dGeometryQuality>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub face_limit: Option<i64>,
|
||||
#[ts(optional = nullable)]
|
||||
pub auto_size: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub orientation: Option<Model3dInputOrientation>,
|
||||
#[ts(optional = nullable)]
|
||||
pub quad: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub smart_low_poly: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub generate_parts: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub compress: Option<Model3dCompression>,
|
||||
#[ts(optional = nullable)]
|
||||
pub export_uv: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub export_orientation: Option<Model3dExportOrientation>,
|
||||
}
|
||||
@@ -1,58 +1,16 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model3d::common::{
|
||||
Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dInputOrientation,
|
||||
Model3dModelVersion, Model3dTextureAlignment, Model3dTextureQuality, Model3dTextureVersion,
|
||||
};
|
||||
|
||||
use super::MODEL3D_TS_EXPORT_DIR;
|
||||
use super::params::Model3dImageToModelParams;
|
||||
use crate::model3d::common::{Model3dGenerationSource, Model3dGenerationTarget};
|
||||
|
||||
/// image-to-model API 请求:站内图片引用 + provider 生成参数 + 平台字段。
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub struct Model3dImageToModelRequest {
|
||||
pub input: String,
|
||||
pub model: Model3dModelVersion,
|
||||
#[ts(optional = nullable)]
|
||||
pub enable_image_autofix: Option<bool>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub model_seed: Option<i64>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_seed: Option<i64>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub pbr: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_quality: Option<Model3dTextureQuality>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_version: Option<Model3dTextureVersion>,
|
||||
#[ts(optional = nullable)]
|
||||
pub delight: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_alignment: Option<Model3dTextureAlignment>,
|
||||
#[ts(optional = nullable)]
|
||||
pub geometry_quality: Option<Model3dGeometryQuality>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub face_limit: Option<i64>,
|
||||
#[ts(optional = nullable)]
|
||||
pub auto_size: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub orientation: Option<Model3dInputOrientation>,
|
||||
#[ts(optional = nullable)]
|
||||
pub quad: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub smart_low_poly: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub generate_parts: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub compress: Option<Model3dCompression>,
|
||||
#[ts(optional = nullable)]
|
||||
pub export_uv: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub export_orientation: Option<Model3dExportOrientation>,
|
||||
pub source: Model3dGenerationSource,
|
||||
pub generation: Model3dImageToModelParams,
|
||||
pub target: Model3dGenerationTarget,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::MODEL3D_TS_EXPORT_DIR;
|
||||
use crate::model3d::common::{Model3dGeneratedArtifact, Model3dGenerationTargetRef};
|
||||
|
||||
/// image-to-model 的完成结果:只包含正式资源引用与已持久化产物。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub struct Model3dImageToModelResult {
|
||||
pub target: Model3dGenerationTargetRef,
|
||||
pub model: Model3dGeneratedArtifact,
|
||||
pub preview: Model3dGeneratedArtifact,
|
||||
}
|
||||
@@ -14,9 +14,13 @@ macro_rules! model3d_ts_export_dir {
|
||||
|
||||
pub(crate) use model3d_ts_export_dir;
|
||||
|
||||
mod generation_result;
|
||||
|
||||
pub mod common;
|
||||
pub mod image_to_model;
|
||||
pub mod multiview_to_model;
|
||||
pub mod text_to_model;
|
||||
|
||||
pub use generation_result::Model3dGenerationResult;
|
||||
|
||||
pub const MODEL3D_CONTRACT_VERSION: &str = "model3d.v1";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
pub(crate) const MODEL3D_TS_EXPORT_DIR: &str =
|
||||
crate::model3d::model3d_ts_export_dir!("text-to-model/");
|
||||
|
||||
mod params;
|
||||
mod request;
|
||||
mod response;
|
||||
mod result;
|
||||
pub use params::Model3dTextToModelParams;
|
||||
pub use request::Model3dTextToModelRequest;
|
||||
pub use response::{
|
||||
Model3dArtifact, Model3dGenerationJob, Model3dGenerationResult, Model3dGenerationSubmission,
|
||||
};
|
||||
pub use result::Model3dTextToModelResult;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::MODEL3D_TS_EXPORT_DIR;
|
||||
use crate::model3d::common::{
|
||||
Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dModelVersion,
|
||||
Model3dTextureQuality, Model3dTextureVersion,
|
||||
};
|
||||
|
||||
/// text-to-model 的 provider 生成参数。
|
||||
///
|
||||
/// 这里保持与 provider 一致的宽松形态(未提供的参数由 provider 取默认值),
|
||||
/// 平台侧的必填口径与组合校验在调用 provider 之前完成,见 `api-server::tripo3d::validation`。
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub struct Model3dTextToModelParams {
|
||||
pub prompt: String,
|
||||
pub model: Model3dModelVersion,
|
||||
#[ts(optional = nullable)]
|
||||
pub negative_prompt: Option<String>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub image_seed: Option<i64>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub model_seed: Option<i64>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_seed: Option<i64>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub pbr: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_quality: Option<Model3dTextureQuality>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_version: Option<Model3dTextureVersion>,
|
||||
#[ts(optional = nullable)]
|
||||
pub delight: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub geometry_quality: Option<Model3dGeometryQuality>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub face_limit: Option<i64>,
|
||||
#[ts(optional = nullable)]
|
||||
pub auto_size: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub quad: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub smart_low_poly: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub generate_parts: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub compress: Option<Model3dCompression>,
|
||||
#[ts(optional = nullable)]
|
||||
pub export_uv: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub export_orientation: Option<Model3dExportOrientation>,
|
||||
}
|
||||
@@ -1,56 +1,18 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::MODEL3D_TS_EXPORT_DIR;
|
||||
use crate::model3d::common::{
|
||||
Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dModelVersion,
|
||||
Model3dTextureQuality, Model3dTextureVersion,
|
||||
};
|
||||
use super::params::Model3dTextToModelParams;
|
||||
use crate::model3d::common::Model3dGenerationTarget;
|
||||
|
||||
/// text-to-model API 请求:provider 生成参数 + 平台字段。
|
||||
///
|
||||
/// 生成参数单独嵌一层而不是摊平到顶层:serde 的 `deny_unknown_fields` 与 `flatten`
|
||||
/// 不能共存,摊平会让顶层未知字段静默通过。
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub struct Model3dTextToModelRequest {
|
||||
pub prompt: String,
|
||||
pub model: Model3dModelVersion,
|
||||
#[ts(optional = nullable)]
|
||||
pub negative_prompt: Option<String>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub image_seed: Option<i64>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub model_seed: Option<i64>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_seed: Option<i64>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub pbr: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_quality: Option<Model3dTextureQuality>,
|
||||
#[ts(optional = nullable)]
|
||||
pub texture_version: Option<Model3dTextureVersion>,
|
||||
#[ts(optional = nullable)]
|
||||
pub delight: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub geometry_quality: Option<Model3dGeometryQuality>,
|
||||
#[ts(type = "number | null")]
|
||||
#[ts(optional = nullable)]
|
||||
pub face_limit: Option<i64>,
|
||||
#[ts(optional = nullable)]
|
||||
pub auto_size: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub quad: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub smart_low_poly: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub generate_parts: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub compress: Option<Model3dCompression>,
|
||||
#[ts(optional = nullable)]
|
||||
pub export_uv: Option<bool>,
|
||||
#[ts(optional = nullable)]
|
||||
pub export_orientation: Option<Model3dExportOrientation>,
|
||||
pub generation: Model3dTextToModelParams,
|
||||
pub target: Model3dGenerationTarget,
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model3d::common::{Model3dOutputFormat, Model3dTaskStatus};
|
||||
|
||||
use super::MODEL3D_TS_EXPORT_DIR;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub struct Model3dGenerationSubmission {
|
||||
pub operation_id: String,
|
||||
pub status: Model3dTaskStatus,
|
||||
pub status_url: String,
|
||||
/// 轮询间隔(毫秒),量级为分钟级,远低于 2^53,按 TS number 导出。
|
||||
#[ts(type = "number")]
|
||||
pub poll_after_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub struct Model3dGenerationJob {
|
||||
pub operation_id: String,
|
||||
pub status: Model3dTaskStatus,
|
||||
pub phase: String,
|
||||
pub progress: u8,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<Model3dGenerationResult>,
|
||||
/// 更新时间(Unix 微秒);2^53 微秒约到公元 2255 年,按 TS number 导出即可精确表示。
|
||||
#[ts(type = "number")]
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub struct Model3dGenerationResult {
|
||||
pub resource_id: String,
|
||||
pub asset_id: String,
|
||||
pub artifacts: Vec<Model3dArtifact>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub struct Model3dArtifact {
|
||||
pub resource_id: String,
|
||||
pub format: Model3dOutputFormat,
|
||||
pub content_type: String,
|
||||
/// 产物字节数,当前单模型产物为几十 MB,远低于 2^53,按 TS number 导出。
|
||||
#[ts(type = "number")]
|
||||
pub size_bytes: u64,
|
||||
pub sha256: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub preview_resource_id: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::MODEL3D_TS_EXPORT_DIR;
|
||||
use crate::model3d::common::{Model3dGeneratedArtifact, Model3dGenerationTargetRef};
|
||||
|
||||
/// text-to-model 的完成结果:只包含正式资源引用与已持久化产物。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(ts_rs::TS)]
|
||||
#[ts(export, export_to = MODEL3D_TS_EXPORT_DIR)]
|
||||
pub struct Model3dTextToModelResult {
|
||||
pub target: Model3dGenerationTargetRef,
|
||||
pub model: Model3dGeneratedArtifact,
|
||||
pub preview: Model3dGeneratedArtifact,
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
use serde_json::json;
|
||||
use shared_contracts::model3d::Model3dGenerationResult;
|
||||
use shared_contracts::model3d::common::{
|
||||
Model3dGeneratedArtifact, Model3dGenerationTarget, Model3dGenerationTargetRef,
|
||||
};
|
||||
use shared_contracts::model3d::image_to_model::Model3dImageToModelRequest;
|
||||
use shared_contracts::model3d::text_to_model::Model3dTextToModelRequest;
|
||||
|
||||
#[test]
|
||||
fn text_to_model_request_accepts_generation_and_target() {
|
||||
let request: Model3dTextToModelRequest = serde_json::from_value(json!({
|
||||
"generation": { "prompt": "一把木椅", "model": "v3.1-20260211", "texture": true },
|
||||
"target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" }
|
||||
}))
|
||||
.expect("合法请求应可反序列化");
|
||||
|
||||
assert_eq!(request.generation.prompt, "一把木椅");
|
||||
assert!(matches!(
|
||||
request.target,
|
||||
Model3dGenerationTarget::AssetLibrary { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_requires_branch_local_fields() {
|
||||
for invalid in [
|
||||
json!({ "kind": "assetLibrary", "label": "木椅" }),
|
||||
json!({ "kind": "assetLibrary", "folderId": "folder-1" }),
|
||||
json!({ "kind": "projectResource" }),
|
||||
] {
|
||||
let error = serde_json::from_value::<Model3dGenerationTarget>(invalid.clone())
|
||||
.expect_err("缺少分支必填字段应被拒绝");
|
||||
assert!(
|
||||
error.to_string().contains("missing field"),
|
||||
"缺少字段的报错应指出 missing field,输入 {invalid},实际为:{error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_source_rejects_ambiguous_and_raw_input() {
|
||||
let both_ids = json!({
|
||||
"source": { "kind": "resource", "resourceId": "r1", "assetId": "a1" },
|
||||
"generation": { "model": "v3.1-20260211" },
|
||||
"target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" }
|
||||
});
|
||||
let error = serde_json::from_value::<Model3dImageToModelRequest>(both_ids)
|
||||
.expect_err("同时给出资源与素材 ID 应被拒绝");
|
||||
assert!(
|
||||
error.to_string().contains("unknown field"),
|
||||
"同时给出两个 ID 应报 unknown field,实际为:{error}"
|
||||
);
|
||||
|
||||
let raw_input = json!({
|
||||
"source": "https://example.com/a.png",
|
||||
"generation": { "model": "v3.1-20260211" },
|
||||
"target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" }
|
||||
});
|
||||
serde_json::from_value::<Model3dImageToModelRequest>(raw_input)
|
||||
.expect_err("裸字符串图片输入应被拒绝");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_request_rejects_unknown_top_level_fields() {
|
||||
let error = serde_json::from_value::<Model3dTextToModelRequest>(json!({
|
||||
"generation": { "prompt": "一把木椅", "model": "v3.1-20260211" },
|
||||
"target": { "kind": "assetLibrary", "folderId": "folder-1", "label": "木椅" },
|
||||
"unexpected": true
|
||||
}))
|
||||
.expect_err("顶层未知字段应被拒绝");
|
||||
|
||||
assert!(
|
||||
error.to_string().contains("unknown field"),
|
||||
"顶层未知字段应报 unknown field,实际为:{error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generation_result_is_tagged_per_endpoint() {
|
||||
let result: Model3dGenerationResult = serde_json::from_value(json!({
|
||||
"kind": "textToModel",
|
||||
"target": { "kind": "projectResource", "resourceId": "res-1" },
|
||||
"model": {
|
||||
"objectKey": "models/res-1.glb",
|
||||
"contentType": "model/gltf-binary",
|
||||
"contentLength": 41_636_076_u64,
|
||||
"sha256": "0f1e"
|
||||
},
|
||||
"preview": {
|
||||
"objectKey": "models/res-1.webp",
|
||||
"contentType": "image/webp",
|
||||
"contentLength": 20_480_u64,
|
||||
"sha256": "2a3b"
|
||||
}
|
||||
}))
|
||||
.expect("按端点的结果应可反序列化");
|
||||
|
||||
let Model3dGenerationResult::TextToModel(text) = result else {
|
||||
panic!("kind=textToModel 不应解成 imageToModel 分支");
|
||||
};
|
||||
assert_eq!(
|
||||
text.target,
|
||||
Model3dGenerationTargetRef::ProjectResource {
|
||||
resource_id: "res-1".into()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
text.model,
|
||||
Model3dGeneratedArtifact {
|
||||
object_key: "models/res-1.glb".into(),
|
||||
content_type: "model/gltf-binary".into(),
|
||||
content_length: 41_636_076,
|
||||
sha256: "0f1e".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user