fix: 修复ComfyUI V2模板创建参数解析问题并重构工作流创建

主要修改:

1. **扩展ComfyUI SDK参数类型支持**
   - 在ParameterType枚举中添加Integer、Float、Image、Audio、Video类型
   - 在ParameterSchema中添加节点映射和媒体文件相关字段
   - 添加step、accept、maxSize、width、height、duration、node_mapping字段
   - 为新字段添加serde默认值支持

2. **修复参数解析问题**
   - 解决comfyui_v2_create_template命令缺少type字段的错误
   - 确保前端传递的参数类型与后端ParameterType枚举匹配
   - 修复r#enum字段的正确使用(用于JSON Schema兼容性)

3. **重构工作流模板创建方式**
   - 将ai_model_face_hair_fix.rs中的create_workflow函数重构为create_workflow_from_json
   - 改为从JSON字符串解析ComfyUIWorkflow,而非手动构建HashMap
   - 提高可维护性和与实际使用场景的一致性

4. **更新相关测试和验证代码**
   - 修复validation.rs中的测试用例,添加新字段的默认值
   - 确保所有ParameterSchema创建都包含完整字段

5. **添加测试数据**
   - 创建test_template_creation.json用于测试参数格式

这些修改解决了前端传递的参数格式与后端期望格式不匹配的问题,
使工作流模板创建功能能够正常工作。
This commit is contained in:
root
2025-08-08 23:10:38 +08:00
parent 824a43f0c3
commit 84081284f7
5 changed files with 466 additions and 344 deletions

View File

@@ -16,7 +16,7 @@ use crate::data::models::comfyui::{
// ==================== 请求和响应类型 ==================== // ==================== 请求和响应类型 ====================
/// 模板创建请求 /// ComfyUI V2 模板创建请求
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateTemplateRequest { pub struct CreateTemplateRequest {
pub name: String, pub name: String,

View File

@@ -13,7 +13,7 @@ use std::collections::HashMap;
use serde_json::json; use serde_json::json;
use crate::types::{ use crate::types::{
WorkflowTemplateData, TemplateMetadata, ParameterSchema, ParameterType, WorkflowTemplateData, TemplateMetadata, ParameterSchema, ParameterType,
ComfyUIWorkflow, ComfyUINode, NodeMeta ComfyUIWorkflow
}; };
/// AI Model Face & Hair Detail Fix Template /// AI Model Face & Hair Detail Fix Template
@@ -52,9 +52,16 @@ fn create_ai_model_face_hair_fix_template() -> WorkflowTemplateData {
r#enum: None, r#enum: None,
min: None, min: None,
max: None, max: None,
step: None,
pattern: None, pattern: None,
items: None, items: None,
properties: None, properties: None,
accept: None,
max_size: None,
width: None,
height: None,
duration: None,
node_mapping: None,
}); });
parameters.insert("face_prompt".to_string(), ParameterSchema { parameters.insert("face_prompt".to_string(), ParameterSchema {
@@ -65,9 +72,16 @@ fn create_ai_model_face_hair_fix_template() -> WorkflowTemplateData {
r#enum: None, r#enum: None,
min: None, min: None,
max: None, max: None,
step: None,
pattern: None, pattern: None,
items: None, items: None,
properties: None, properties: None,
accept: None,
max_size: None,
width: None,
height: None,
duration: None,
node_mapping: None,
}); });
parameters.insert("face_denoise".to_string(), ParameterSchema { parameters.insert("face_denoise".to_string(), ParameterSchema {
@@ -78,12 +92,19 @@ fn create_ai_model_face_hair_fix_template() -> WorkflowTemplateData {
r#enum: None, r#enum: None,
min: None, min: None,
max: None, max: None,
step: None,
pattern: None, pattern: None,
items: None, items: None,
properties: None, properties: None,
accept: None,
max_size: None,
width: None,
height: None,
duration: None,
node_mapping: None,
}); });
let workflow = create_workflow(); let workflow = create_workflow_from_json();
WorkflowTemplateData { WorkflowTemplateData {
metadata, metadata,
@@ -92,344 +113,277 @@ fn create_ai_model_face_hair_fix_template() -> WorkflowTemplateData {
} }
} }
fn create_workflow() -> ComfyUIWorkflow { fn create_workflow_from_json() -> ComfyUIWorkflow {
let mut workflow = HashMap::new(); let workflow_json = r#"
{
// Node 6: 遮罩边缘滑条快速模糊 "6": {
workflow.insert("6".to_string(), ComfyUINode { "inputs": {
class_type: "EG_ZZ_MHHT".to_string(), "模糊强度": 3,
inputs: { "mask": ["53", 1]
let mut inputs = HashMap::new(); },
inputs.insert("模糊强度".to_string(), json!(3)); "class_type": "EG_ZZ_MHHT",
inputs.insert("mask".to_string(), json!(["53", 1])); "_meta": {
inputs "title": "2🐕遮罩边缘滑条快速模糊"
}, }
_meta: Some(NodeMeta { },
title: Some("2🐕遮罩边缘滑条快速模糊".to_string()), "8": {
}), "inputs": {
}); "ckpt_name": "juggernaut_reborn_sd1,5.safetensors"
},
// Node 8: Checkpoint加载器 "class_type": "CheckpointLoaderSimple",
workflow.insert("8".to_string(), ComfyUINode { "_meta": {
class_type: "CheckpointLoaderSimple".to_string(), "title": "Checkpoint加载器简易"
inputs: { }
let mut inputs = HashMap::new(); },
inputs.insert("ckpt_name".to_string(), json!("juggernaut_reborn_sd1,5.safetensors")); "9": {
inputs "inputs": {
}, "text": "Smooth long hair,Hair details, quality hair,Smooth hair",
_meta: Some(NodeMeta { "clip": ["8", 1]
title: Some("Checkpoint加载器简易".to_string()), },
}), "class_type": "CLIPTextEncode",
}); "_meta": {
"title": "CLIP文本编码"
// Node 9: CLIP文本编码 (头发) }
workflow.insert("9".to_string(), ComfyUINode { },
class_type: "CLIPTextEncode".to_string(), "10": {
inputs: { "inputs": {
let mut inputs = HashMap::new(); "text": "Frizzy hair, dry hair, split ends,bad quality, poor quality, doll, disfigured, jpg, toy, bad anatomy, missing limbs, missing fingers, 3d, cgi",
inputs.insert("text".to_string(), json!("Smooth long hair,Hair details, quality hair,Smooth hair")); "clip": ["8", 1]
inputs.insert("clip".to_string(), json!(["8", 1])); },
inputs "class_type": "CLIPTextEncode",
}, "_meta": {
_meta: Some(NodeMeta { "title": "CLIP文本编码"
title: Some("CLIP文本编码".to_string()), }
}), },
}); "13": {
"inputs": {
// Node 10: CLIP文本编码 (负面头发) "text": ["59", 0],
workflow.insert("10".to_string(), ComfyUINode { "clip": ["8", 1]
class_type: "CLIPTextEncode".to_string(), },
inputs: { "class_type": "CLIPTextEncode",
let mut inputs = HashMap::new(); "_meta": {
inputs.insert("text".to_string(), json!("Frizzy hair, dry hair, split ends,bad quality, poor quality, doll, disfigured, jpg, toy, bad anatomy, missing limbs, missing fingers, 3d, cgi")); "title": "CLIP文本编码"
inputs.insert("clip".to_string(), json!(["8", 1])); }
inputs },
}, "14": {
_meta: Some(NodeMeta { "inputs": {
title: Some("CLIP文本编码".to_string()), "text": "(NSFW:1.2),(worst quality:1.2),(low quality:1.2),(normal quality:1.2),low resolution,watermark,",
}), "clip": ["8", 1]
}); },
"class_type": "CLIPTextEncode",
// Node 13: CLIP文本编码 (脸部) "_meta": {
workflow.insert("13".to_string(), ComfyUINode { "title": "CLIP文本编码"
class_type: "CLIPTextEncode".to_string(), }
inputs: { },
let mut inputs = HashMap::new(); "22": {
inputs.insert("text".to_string(), json!(["59", 0])); "inputs": {
inputs.insert("clip".to_string(), json!(["8", 1])); "detail_method": "VITMatte(local)",
inputs "detail_erode": 44,
}, "detail_dilate": 6,
_meta: Some(NodeMeta { "black_point": 0.030000000000000006,
title: Some("CLIP文本编码".to_string()), "white_point": 0.99,
}), "process_detail": true,
}); "device": "cuda",
"max_megapixels": 2,
// Node 14: CLIP文本编码 (负面脸部) "image": ["30", 0],
workflow.insert("14".to_string(), ComfyUINode { "segformer_pipeline": ["28", 0]
class_type: "CLIPTextEncode".to_string(), },
inputs: { "class_type": "LayerMask: SegformerUltraV2",
let mut inputs = HashMap::new(); "_meta": {
inputs.insert("text".to_string(), json!("(NSFW:1.2),(worst quality:1.2),(low quality:1.2),(normal quality:1.2),low resolution,watermark,")); "title": "LayerMask: Segformer Ultra V2"
inputs.insert("clip".to_string(), json!(["8", 1])); }
inputs },
}, "25": {
_meta: Some(NodeMeta { "inputs": {
title: Some("CLIP文本编码".to_string()), "seed": 646617836820462,
}), "steps": 30,
}); "cfg": 4.5200000000000005,
"sampler_name": "dpmpp_2s_ancestral",
// Node 22: Segformer Ultra V2 (头发) "scheduler": "karras",
workflow.insert("22".to_string(), ComfyUINode { "denoise": 0.4,
class_type: "LayerMask: SegformerUltraV2".to_string(), "重绘模式": "原图",
inputs: { "遮罩延展": 10,
let mut inputs = HashMap::new(); "仅局部重绘": true,
inputs.insert("detail_method".to_string(), json!("VITMatte(local)")); "局部重绘大小": 768,
inputs.insert("detail_erode".to_string(), json!(44)); "重绘区域扩展": 50,
inputs.insert("detail_dilate".to_string(), json!(6)); "遮罩羽化": 5,
inputs.insert("black_point".to_string(), json!(0.030000000000000006)); "TEXT": "2🐕出品必出精品",
inputs.insert("white_point".to_string(), json!(0.99)); "model": ["8", 0],
inputs.insert("process_detail".to_string(), json!(true)); "image": ["30", 0],
inputs.insert("device".to_string(), json!("cuda")); "vae": ["8", 2],
inputs.insert("max_megapixels".to_string(), json!(2)); "mask": ["47", 0],
inputs.insert("image".to_string(), json!(["30", 0])); "positive": ["9", 0],
inputs.insert("segformer_pipeline".to_string(), json!(["28", 0])); "negative": ["10", 0]
inputs },
}, "class_type": "EG_CYQ_JB",
_meta: Some(NodeMeta { "_meta": {
title: Some("LayerMask: Segformer Ultra V2".to_string()), "title": "2🐕局部重绘采样器"
}), }
}); },
"26": {
// Node 25: 局部重绘采样器 (头发) "inputs": {
workflow.insert("25".to_string(), ComfyUINode { "seed": 969968724502727,
class_type: "EG_CYQ_JB".to_string(), "steps": 30,
inputs: { "cfg": 4.5200000000000005,
let mut inputs = HashMap::new(); "sampler_name": "dpmpp_2s_ancestral",
inputs.insert("seed".to_string(), json!(646617836820462i64)); "scheduler": "karras",
inputs.insert("steps".to_string(), json!(30)); "denoise": ["71", 0],
inputs.insert("cfg".to_string(), json!(4.5200000000000005)); "重绘模式": "原图",
inputs.insert("sampler_name".to_string(), json!("dpmpp_2s_ancestral")); "遮罩延展": 10,
inputs.insert("scheduler".to_string(), json!("karras")); "仅局部重绘": true,
inputs.insert("denoise".to_string(), json!(0.4)); "局部重绘大小": 768,
inputs.insert("重绘模式".to_string(), json!("原图")); "重绘区域扩展": 50,
inputs.insert("遮罩延展".to_string(), json!(10)); "遮罩羽化": 5,
inputs.insert("仅局部重绘".to_string(), json!(true)); "TEXT": "2🐕出品必出精品",
inputs.insert("局部重绘大小".to_string(), json!(768)); "model": ["8", 0],
inputs.insert("重绘区域扩展".to_string(), json!(50)); "image": ["25", 1],
inputs.insert("遮罩羽化".to_string(), json!(5)); "vae": ["8", 2],
inputs.insert("TEXT".to_string(), json!("2🐕出品必出精品")); "mask": ["6", 0],
inputs.insert("model".to_string(), json!(["8", 0])); "positive": ["13", 0],
inputs.insert("image".to_string(), json!(["30", 0])); "negative": ["14", 0]
inputs.insert("vae".to_string(), json!(["8", 2])); },
inputs.insert("mask".to_string(), json!(["47", 0])); "class_type": "EG_CYQ_JB",
inputs.insert("positive".to_string(), json!(["9", 0])); "_meta": {
inputs.insert("negative".to_string(), json!(["10", 0])); "title": "2🐕局部重绘采样器"
inputs }
}, },
_meta: Some(NodeMeta { "28": {
title: Some("2🐕局部重绘采样器".to_string()), "inputs": {
}), "model": "segformer_b3_clothes",
}); "face": false,
"hair": true,
// Node 26: 局部重绘采样器 (脸部) "hat": false,
workflow.insert("26".to_string(), ComfyUINode { "sunglass": false,
class_type: "EG_CYQ_JB".to_string(), "left_arm": false,
inputs: { "right_arm": false,
let mut inputs = HashMap::new(); "left_leg": false,
inputs.insert("seed".to_string(), json!(969968724502727i64)); "right_leg": false,
inputs.insert("steps".to_string(), json!(30)); "left_shoe": false,
inputs.insert("cfg".to_string(), json!(4.5200000000000005)); "right_shoe": false,
inputs.insert("sampler_name".to_string(), json!("dpmpp_2s_ancestral")); "upper_clothes": false,
inputs.insert("scheduler".to_string(), json!("karras")); "skirt": false,
inputs.insert("denoise".to_string(), json!(["71", 0])); "pants": false,
inputs.insert("重绘模式".to_string(), json!("原图")); "dress": false,
inputs.insert("遮罩延展".to_string(), json!(10)); "belt": false,
inputs.insert("仅局部重绘".to_string(), json!(true)); "bag": false,
inputs.insert("局部重绘大小".to_string(), json!(768)); "scarf": false
inputs.insert("重绘区域扩展".to_string(), json!(50)); },
inputs.insert("遮罩羽化".to_string(), json!(5)); "class_type": "LayerMask: SegformerClothesPipelineLoader",
inputs.insert("TEXT".to_string(), json!("2🐕出品必出精品")); "_meta": {
inputs.insert("model".to_string(), json!(["8", 0])); "title": "LayerMask: Segformer Clothes Pipeline"
inputs.insert("image".to_string(), json!(["25", 1])); }
inputs.insert("vae".to_string(), json!(["8", 2])); },
inputs.insert("mask".to_string(), json!(["6", 0])); "30": {
inputs.insert("positive".to_string(), json!(["13", 0])); "inputs": {
inputs.insert("negative".to_string(), json!(["14", 0])); "image": "{{input_image}}"
inputs },
}, "class_type": "LoadImage",
_meta: Some(NodeMeta { "_meta": {
title: Some("2🐕局部重绘采样器".to_string()), "title": "加载图像"
}), }
}); },
"44": {
// Node 28: Segformer Clothes Pipeline (头发) "inputs": {
workflow.insert("28".to_string(), ComfyUINode { "masks": ["22", 1]
class_type: "LayerMask: SegformerClothesPipelineLoader".to_string(), },
inputs: { "class_type": "Mask Fill Holes",
let mut inputs = HashMap::new(); "_meta": {
inputs.insert("model".to_string(), json!("segformer_b3_clothes")); "title": "Mask Fill Holes"
inputs.insert("face".to_string(), json!(false)); }
inputs.insert("hair".to_string(), json!(true)); },
inputs.insert("hat".to_string(), json!(false)); "47": {
inputs.insert("sunglass".to_string(), json!(false)); "inputs": {
inputs.insert("left_arm".to_string(), json!(false)); "expand": 5,
inputs.insert("right_arm".to_string(), json!(false)); "incremental_expandrate": 5,
inputs.insert("left_leg".to_string(), json!(false)); "tapered_corners": true,
inputs.insert("right_leg".to_string(), json!(false)); "flip_input": false,
inputs.insert("left_shoe".to_string(), json!(false)); "blur_radius": 9.4,
inputs.insert("right_shoe".to_string(), json!(false)); "lerp_alpha": 0.9900000000000002,
inputs.insert("upper_clothes".to_string(), json!(false)); "decay_factor": 1,
inputs.insert("skirt".to_string(), json!(false)); "fill_holes": false,
inputs.insert("pants".to_string(), json!(false)); "mask": ["44", 0]
inputs.insert("dress".to_string(), json!(false)); },
inputs.insert("belt".to_string(), json!(false)); "class_type": "GrowMaskWithBlur",
inputs.insert("bag".to_string(), json!(false)); "_meta": {
inputs.insert("scarf".to_string(), json!(false)); "title": "Grow Mask With Blur"
inputs }
}, },
_meta: Some(NodeMeta { "52": {
title: Some("LayerMask: Segformer Clothes Pipeline".to_string()), "inputs": {
}), "model": "segformer_b3_clothes",
}); "face": true,
"hair": false,
// Node 30: 加载图像 "hat": false,
workflow.insert("30".to_string(), ComfyUINode { "sunglass": true,
class_type: "LoadImage".to_string(), "left_arm": false,
inputs: { "right_arm": false,
let mut inputs = HashMap::new(); "left_leg": false,
inputs.insert("image".to_string(), json!("{{input_image}}")); "right_leg": false,
inputs "left_shoe": false,
}, "right_shoe": false,
_meta: Some(NodeMeta { "upper_clothes": false,
title: Some("加载图像".to_string()), "skirt": false,
}), "pants": false,
}); "dress": false,
"belt": false,
// Node 44: Mask Fill Holes "bag": false,
workflow.insert("44".to_string(), ComfyUINode { "scarf": false
class_type: "Mask Fill Holes".to_string(), },
inputs: { "class_type": "LayerMask: SegformerClothesPipelineLoader",
let mut inputs = HashMap::new(); "_meta": {
inputs.insert("masks".to_string(), json!(["22", 1])); "title": "LayerMask: Segformer Clothes Pipeline"
inputs }
}, },
_meta: Some(NodeMeta { "53": {
title: Some("Mask Fill Holes".to_string()), "inputs": {
}), "detail_method": "VITMatte(local)",
}); "detail_erode": 6,
"detail_dilate": 4,
// Node 47: Grow Mask With Blur "black_point": 0.01,
workflow.insert("47".to_string(), ComfyUINode { "white_point": 0.99,
class_type: "GrowMaskWithBlur".to_string(), "process_detail": false,
inputs: { "device": "cuda",
let mut inputs = HashMap::new(); "max_megapixels": 2,
inputs.insert("expand".to_string(), json!(5)); "image": ["30", 0],
inputs.insert("incremental_expandrate".to_string(), json!(5)); "segformer_pipeline": ["52", 0]
inputs.insert("tapered_corners".to_string(), json!(true)); },
inputs.insert("flip_input".to_string(), json!(false)); "class_type": "LayerMask: SegformerUltraV2",
inputs.insert("blur_radius".to_string(), json!(9.4)); "_meta": {
inputs.insert("lerp_alpha".to_string(), json!(0.9900000000000002)); "title": "LayerMask: Segformer Ultra V2"
inputs.insert("decay_factor".to_string(), json!(1)); }
inputs.insert("fill_holes".to_string(), json!(false)); },
inputs.insert("mask".to_string(), json!(["44", 0])); "58": {
inputs "inputs": {
}, "filename_prefix": "ComfyUI",
_meta: Some(NodeMeta { "images": ["26", 1]
title: Some("Grow Mask With Blur".to_string()), },
}), "class_type": "SaveImage",
}); "_meta": {
"title": "保存图像"
// Node 52: Segformer Clothes Pipeline (脸部) }
workflow.insert("52".to_string(), ComfyUINode { },
class_type: "LayerMask: SegformerClothesPipelineLoader".to_string(), "59": {
inputs: { "inputs": {
let mut inputs = HashMap::new(); "String": "{{face_prompt}}"
inputs.insert("model".to_string(), json!("segformer_b3_clothes")); },
inputs.insert("face".to_string(), json!(true)); "class_type": "String",
inputs.insert("hair".to_string(), json!(false)); "_meta": {
inputs.insert("hat".to_string(), json!(false)); "title": "String"
inputs.insert("sunglass".to_string(), json!(true)); }
inputs.insert("left_arm".to_string(), json!(false)); },
inputs.insert("right_arm".to_string(), json!(false)); "71": {
inputs.insert("left_leg".to_string(), json!(false)); "inputs": {
inputs.insert("right_leg".to_string(), json!(false)); "Number": "{{face_denoise}}"
inputs.insert("left_shoe".to_string(), json!(false)); },
inputs.insert("right_shoe".to_string(), json!(false)); "class_type": "Float",
inputs.insert("upper_clothes".to_string(), json!(false)); "_meta": {
inputs.insert("skirt".to_string(), json!(false)); "title": "Float"
inputs.insert("pants".to_string(), json!(false)); }
inputs.insert("dress".to_string(), json!(false)); }
inputs.insert("belt".to_string(), json!(false)); }
inputs.insert("bag".to_string(), json!(false)); "#;
inputs.insert("scarf".to_string(), json!(false));
inputs // 解析 JSON 字符串为 ComfyUIWorkflow
}, serde_json::from_str(workflow_json)
_meta: Some(NodeMeta { .expect("Failed to parse workflow JSON")
title: Some("LayerMask: Segformer Clothes Pipeline".to_string()),
}),
});
// Node 53: Segformer Ultra V2 (脸部)
workflow.insert("53".to_string(), ComfyUINode {
class_type: "LayerMask: SegformerUltraV2".to_string(),
inputs: {
let mut inputs = HashMap::new();
inputs.insert("detail_method".to_string(), json!("VITMatte(local)"));
inputs.insert("detail_erode".to_string(), json!(6));
inputs.insert("detail_dilate".to_string(), json!(4));
inputs.insert("black_point".to_string(), json!(0.01));
inputs.insert("white_point".to_string(), json!(0.99));
inputs.insert("process_detail".to_string(), json!(false));
inputs.insert("device".to_string(), json!("cuda"));
inputs.insert("max_megapixels".to_string(), json!(2));
inputs.insert("image".to_string(), json!(["30", 0]));
inputs.insert("segformer_pipeline".to_string(), json!(["52", 0]));
inputs
},
_meta: Some(NodeMeta {
title: Some("LayerMask: Segformer Ultra V2".to_string()),
}),
});
// Node 58: 保存图像
workflow.insert("58".to_string(), ComfyUINode {
class_type: "SaveImage".to_string(),
inputs: {
let mut inputs = HashMap::new();
inputs.insert("filename_prefix".to_string(), json!("ComfyUI"));
inputs.insert("images".to_string(), json!(["26", 1]));
inputs
},
_meta: Some(NodeMeta {
title: Some("保存图像".to_string()),
}),
});
// Node 59: String (脸部提示词)
workflow.insert("59".to_string(), ComfyUINode {
class_type: "String".to_string(),
inputs: {
let mut inputs = HashMap::new();
inputs.insert("String".to_string(), json!("A girl, slim figure, oval face, beauty, Pink and greasy lips{{face_prompt}}"));
inputs
},
_meta: Some(NodeMeta {
title: Some("String".to_string()),
}),
});
// Node 71: Float (脸部去噪强度)
workflow.insert("71".to_string(), ComfyUINode {
class_type: "Float".to_string(),
inputs: {
let mut inputs = HashMap::new();
inputs.insert("Number".to_string(), json!("{{face_denoise}}"));
inputs
},
_meta: Some(NodeMeta {
title: Some("Float".to_string()),
}),
});
workflow
} }

View File

@@ -39,12 +39,23 @@ pub enum ParameterType {
Boolean, Boolean,
Array, Array,
Object, Object,
Integer,
Float,
Image,
Audio,
Video,
}
/// Node mapping configuration for parameter-to-workflow binding
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeMapping {
pub node_id: String,
pub input_field: String,
} }
/// Parameter schema definition /// Parameter schema definition
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterSchema { pub struct ParameterSchema {
#[serde(rename = "type")]
pub param_type: ParameterType, pub param_type: ParameterType,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>, pub required: Option<bool>,
@@ -59,11 +70,35 @@ pub struct ParameterSchema {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>, pub max: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub step: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>, pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<Box<ParameterSchema>>, pub items: Option<Box<ParameterSchema>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, ParameterSchema>>, pub properties: Option<HashMap<String, ParameterSchema>>,
// Media file related properties
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub accept: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "maxSize")]
#[serde(default)]
pub max_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub height: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub duration: Option<f64>,
// Workflow node mapping
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub node_mapping: Option<NodeMapping>,
} }
/// Template metadata /// Template metadata
@@ -190,3 +225,28 @@ impl ValidationError {
} }
} }
} }
impl ParameterSchema {
/// Creates a new ParameterSchema with the given type and default values for other fields
pub fn new(param_type: ParameterType) -> Self {
Self {
param_type,
required: None,
default: None,
description: None,
r#enum: None,
min: None,
max: None,
step: None,
pattern: None,
items: None,
properties: None,
accept: None,
max_size: None,
width: None,
height: None,
duration: None,
node_mapping: None,
}
}
}

View File

@@ -235,9 +235,16 @@ mod tests {
r#enum: None, r#enum: None,
min: None, min: None,
max: None, max: None,
step: None,
pattern: None, pattern: None,
items: None, items: None,
properties: None, properties: None,
accept: None,
max_size: None,
width: None,
height: None,
duration: None,
node_mapping: None,
}); });
let parameters = HashMap::new(); let parameters = HashMap::new();
@@ -259,9 +266,16 @@ mod tests {
r#enum: None, r#enum: None,
min: Some(3.0), min: Some(3.0),
max: Some(10.0), max: Some(10.0),
step: None,
pattern: None, pattern: None,
items: None, items: None,
properties: None, properties: None,
accept: None,
max_size: None,
width: None,
height: None,
duration: None,
node_mapping: None,
}); });
let mut parameters = HashMap::new(); let mut parameters = HashMap::new();

View File

@@ -0,0 +1,94 @@
{
"request": {
"name": "AI模型头发修复细节",
"category": "",
"description": "",
"template_data": {
"metadata": {
"id": "ai-001",
"name": "AI模型头发修复细节",
"description": "",
"version": "1.0.0",
"author": "",
"tags": [],
"category": ""
},
"workflow": {
"30": {
"inputs": {
"image": "{{input_image}}"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"71": {
"inputs": {
"Number": "{{prompt_strage}}"
},
"class_type": "Float",
"_meta": {
"title": "Float"
}
}
},
"parameters": {
"input_image": {
"param_type": "image",
"required": false,
"description": "支持的图片格式JPG, PNG, WebP, BMP, TIFF",
"default": "",
"node_mapping": {
"node_id": "30",
"input_field": "image"
},
"accept": ".jpg,.jpeg,.png,.webp,.bmp,.tiff",
"maxSize": 10485760
},
"prompt_strage": {
"param_type": "float",
"required": false,
"description": "",
"default": 0.3,
"min": 0,
"max": 1,
"step": 0.01,
"node_mapping": {
"node_id": "71",
"input_field": "Number"
}
}
}
},
"parameter_schema": {
"input_image": {
"param_type": "image",
"required": false,
"description": "支持的图片格式JPG, PNG, WebP, BMP, TIFF",
"default": "",
"node_mapping": {
"node_id": "30",
"input_field": "image"
},
"accept": ".jpg,.jpeg,.png,.webp,.bmp,.tiff",
"maxSize": 10485760
},
"prompt_strage": {
"param_type": "float",
"required": false,
"description": "",
"default": 0.3,
"min": 0,
"max": 1,
"step": 0.01,
"node_mapping": {
"node_id": "71",
"input_field": "Number"
}
}
},
"tags": [],
"author": ""
}
}