refactor(workflow_ui): 抽出 models 子模块
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,12 @@ use node_defs::{
|
|||||||
inject_boosapi_workflow_template_payload, node_type_def_to_comfy, normalize_prompt_payload,
|
inject_boosapi_workflow_template_payload, node_type_def_to_comfy, normalize_prompt_payload,
|
||||||
text_to_video_workflow_graph, workflow_template_payload,
|
text_to_video_workflow_graph, workflow_template_payload,
|
||||||
};
|
};
|
||||||
|
mod models;
|
||||||
|
use models::{
|
||||||
|
combo_fields_from_node, image_model_choices, is_video_workflow_model,
|
||||||
|
native_folder_models_from_object_info, workflow_folder_has_route_models,
|
||||||
|
workflow_folder_is_api_only, workflow_folder_route_model_items, workflow_route_models,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn routes() -> Router<AppState> {
|
pub fn routes() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
@@ -383,302 +389,6 @@ fn parse_boolish(input: Option<&str>) -> bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_exact_workflow_model(value: &str) -> bool {
|
|
||||||
let normalized = value.trim();
|
|
||||||
!normalized.is_empty()
|
|
||||||
&& !normalized.to_ascii_lowercase().starts_with("re:")
|
|
||||||
&& !normalized.contains('*')
|
|
||||||
&& !normalized.contains('?')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_image_workflow_model(value: &str) -> bool {
|
|
||||||
if !is_exact_workflow_model(value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let normalized = value.trim().to_ascii_lowercase();
|
|
||||||
let non_image_keywords = [
|
|
||||||
"tts",
|
|
||||||
"voice",
|
|
||||||
"audio",
|
|
||||||
"embedding",
|
|
||||||
"embed",
|
|
||||||
"seedance",
|
|
||||||
"wan2",
|
|
||||||
"t2v",
|
|
||||||
"i2v",
|
|
||||||
"flf2v",
|
|
||||||
"seaweed",
|
|
||||||
"3d",
|
|
||||||
"hitem3d",
|
|
||||||
"hyper3d",
|
|
||||||
];
|
|
||||||
if non_image_keywords
|
|
||||||
.iter()
|
|
||||||
.any(|keyword| normalized.contains(keyword))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let image_keywords = [
|
|
||||||
"gpt-image",
|
|
||||||
"dall-e",
|
|
||||||
"dalle",
|
|
||||||
"t2i",
|
|
||||||
"i2i",
|
|
||||||
"midjourney",
|
|
||||||
"recraft",
|
|
||||||
"kolors",
|
|
||||||
"cogview",
|
|
||||||
"seedream",
|
|
||||||
"sdxl",
|
|
||||||
"stable-diffusion",
|
|
||||||
"stablediffusion",
|
|
||||||
"pixart",
|
|
||||||
"playground",
|
|
||||||
"firefly",
|
|
||||||
"imagen",
|
|
||||||
"flux",
|
|
||||||
"image",
|
|
||||||
"img",
|
|
||||||
"canvas",
|
|
||||||
"painting",
|
|
||||||
"draw",
|
|
||||||
];
|
|
||||||
image_keywords
|
|
||||||
.iter()
|
|
||||||
.any(|keyword| normalized.contains(keyword))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_video_workflow_model(value: &str) -> bool {
|
|
||||||
let normalized = value.trim().to_ascii_lowercase();
|
|
||||||
[
|
|
||||||
"seedance", "wan2", "t2v", "i2v", "flf2v", "video", "seaweed",
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.any(|keyword| normalized.contains(keyword))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_audio_workflow_model(value: &str) -> bool {
|
|
||||||
let normalized = value.trim().to_ascii_lowercase();
|
|
||||||
["tts", "voice", "audio", "speech"]
|
|
||||||
.iter()
|
|
||||||
.any(|keyword| normalized.contains(keyword))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_embedding_workflow_model(value: &str) -> bool {
|
|
||||||
let normalized = value.trim().to_ascii_lowercase();
|
|
||||||
normalized.contains("embedding") || normalized.contains("embed")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_3d_workflow_model(value: &str) -> bool {
|
|
||||||
let normalized = value.trim().to_ascii_lowercase();
|
|
||||||
["3d", "hitem3d", "hyper3d"]
|
|
||||||
.iter()
|
|
||||||
.any(|keyword| normalized.contains(keyword))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_vision_workflow_model(value: &str) -> bool {
|
|
||||||
let normalized = value.trim().to_ascii_lowercase();
|
|
||||||
["vision", "vl", "omni", "ui-tars"]
|
|
||||||
.iter()
|
|
||||||
.any(|keyword| normalized.contains(keyword))
|
|
||||||
&& !is_image_workflow_model(value)
|
|
||||||
&& !is_video_workflow_model(value)
|
|
||||||
&& !is_embedding_workflow_model(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_chat_workflow_model(value: &str) -> bool {
|
|
||||||
is_exact_workflow_model(value)
|
|
||||||
&& !is_image_workflow_model(value)
|
|
||||||
&& !is_video_workflow_model(value)
|
|
||||||
&& !is_audio_workflow_model(value)
|
|
||||||
&& !is_embedding_workflow_model(value)
|
|
||||||
&& !is_3d_workflow_model(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn workflow_route_models(state: &AppState) -> AppResult<Vec<String>> {
|
|
||||||
let rows = sqlx::query_scalar::<_, String>(
|
|
||||||
"SELECT DISTINCT model_pattern
|
|
||||||
FROM token_routes
|
|
||||||
WHERE COALESCE(enabled, 1) = 1
|
|
||||||
AND model_pattern IS NOT NULL
|
|
||||||
AND model_pattern <> ''
|
|
||||||
ORDER BY model_pattern COLLATE NOCASE",
|
|
||||||
)
|
|
||||||
.fetch_all(&*state.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut seen = std::collections::HashSet::new();
|
|
||||||
Ok(rows
|
|
||||||
.into_iter()
|
|
||||||
.map(|value| value.trim().to_string())
|
|
||||||
.filter(|value| is_exact_workflow_model(value))
|
|
||||||
.filter(|value| seen.insert(value.clone()))
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn image_model_choices(models: &[String]) -> Vec<String> {
|
|
||||||
let image_models = models
|
|
||||||
.iter()
|
|
||||||
.filter(|model| is_image_workflow_model(model))
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
if image_models.is_empty() {
|
|
||||||
models.to_vec()
|
|
||||||
} else {
|
|
||||||
image_models
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workflow_folder_models(folder: &str, models: &[String]) -> Vec<String> {
|
|
||||||
let normalized = folder.trim().to_ascii_lowercase();
|
|
||||||
match normalized.as_str() {
|
|
||||||
"boosapi_models" | "boosapi" | "api_models" => models.to_vec(),
|
|
||||||
"image_models" => image_model_choices(models),
|
|
||||||
"chat_models" => models
|
|
||||||
.iter()
|
|
||||||
.filter(|model| is_chat_workflow_model(model))
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
"clip" => models.to_vec(),
|
|
||||||
// Comfy's video_models folder is a native Comfy API model folder. Keep it
|
|
||||||
// separate from BoosAPI route models so video apps submit real Comfy API
|
|
||||||
// model names instead of OpenAI-compatible route aliases.
|
|
||||||
"video_models" => Vec::new(),
|
|
||||||
"audio_tts_models" => models
|
|
||||||
.iter()
|
|
||||||
.filter(|model| is_audio_workflow_model(model))
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
"embeddings" => models
|
|
||||||
.iter()
|
|
||||||
.filter(|model| is_embedding_workflow_model(model))
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
"3d_models" => models
|
|
||||||
.iter()
|
|
||||||
.filter(|model| is_3d_workflow_model(model))
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
"vision_models" => models
|
|
||||||
.iter()
|
|
||||||
.filter(|model| is_vision_workflow_model(model))
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
_ => Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workflow_folder_is_api_only(folder: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
folder.trim().to_ascii_lowercase().as_str(),
|
|
||||||
"boosapi" | "boosapi_models" | "api_models"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workflow_folder_has_route_models(folder: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
folder.trim().to_ascii_lowercase().as_str(),
|
|
||||||
"boosapi"
|
|
||||||
| "boosapi_models"
|
|
||||||
| "api_models"
|
|
||||||
| "image_models"
|
|
||||||
| "chat_models"
|
|
||||||
| "audio_tts_models"
|
|
||||||
| "embeddings"
|
|
||||||
| "3d_models"
|
|
||||||
| "vision_models"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workflow_folder_route_model_items(folder: &str, route_models: &[String]) -> Vec<Value> {
|
|
||||||
model_payload_items_from_names(workflow_folder_models(folder, route_models), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn combo_choices_from_node_input(
|
|
||||||
object_info: &Value,
|
|
||||||
node_type: &str,
|
|
||||||
input_name: &str,
|
|
||||||
) -> Vec<String> {
|
|
||||||
object_info
|
|
||||||
.get(node_type)
|
|
||||||
.and_then(|node| {
|
|
||||||
node.pointer(&format!("/input/required/{input_name}"))
|
|
||||||
.or_else(|| node.pointer(&format!("/input/optional/{input_name}")))
|
|
||||||
})
|
|
||||||
.and_then(Value::as_array)
|
|
||||||
.and_then(|input| input.first())
|
|
||||||
.and_then(Value::as_array)
|
|
||||||
.map(|choices| {
|
|
||||||
choices
|
|
||||||
.iter()
|
|
||||||
.filter_map(Value::as_str)
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty() && *value != "None")
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn combo_fields_from_node(object_info: &Value, node_type: &str) -> Vec<(String, Vec<String>)> {
|
|
||||||
let mut fields = Vec::new();
|
|
||||||
let Some(node) = object_info.get(node_type) else {
|
|
||||||
return fields;
|
|
||||||
};
|
|
||||||
for section in ["required", "optional"] {
|
|
||||||
if let Some(inputs) = node
|
|
||||||
.pointer(&format!("/input/{section}"))
|
|
||||||
.and_then(Value::as_object)
|
|
||||||
{
|
|
||||||
for (name, value) in inputs {
|
|
||||||
let Some(choices) = value
|
|
||||||
.as_array()
|
|
||||||
.and_then(|input| input.first())
|
|
||||||
.and_then(Value::as_array)
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let choices = choices
|
|
||||||
.iter()
|
|
||||||
.filter_map(Value::as_str)
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty() && *value != "None")
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
fields.push((name.clone(), choices));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fields
|
|
||||||
}
|
|
||||||
|
|
||||||
fn native_folder_models_from_object_info(folder: &str, object_info: &Value) -> Vec<String> {
|
|
||||||
let normalized = folder.trim().to_ascii_lowercase();
|
|
||||||
match normalized.as_str() {
|
|
||||||
"checkpoints" | "image_models" => {
|
|
||||||
combo_choices_from_node_input(object_info, "CheckpointLoaderSimple", "ckpt_name")
|
|
||||||
}
|
|
||||||
"loras" => combo_choices_from_node_input(object_info, "LoraLoader", "lora_name"),
|
|
||||||
"vae" => combo_choices_from_node_input(object_info, "VAELoader", "vae_name"),
|
|
||||||
"clip" | "clip_models" | "text_encoders" => {
|
|
||||||
combo_choices_from_node_input(object_info, "CLIPLoader", "clip_name")
|
|
||||||
}
|
|
||||||
"unet" | "diffusion_models" | "diffusion_models_files" => {
|
|
||||||
combo_choices_from_node_input(object_info, "UNETLoader", "unet_name")
|
|
||||||
}
|
|
||||||
"controlnet" | "controlnets" => {
|
|
||||||
combo_choices_from_node_input(object_info, "ControlNetLoader", "control_net_name")
|
|
||||||
}
|
|
||||||
"upscale_models" => {
|
|
||||||
combo_choices_from_node_input(object_info, "UpscaleModelLoader", "model_name")
|
|
||||||
}
|
|
||||||
"embeddings" => combo_choices_from_node_input(object_info, "EmbeddingPicker", "embedding"),
|
|
||||||
_ => Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn comfy_combo_input(options: &[String], fallback: &str) -> Value {
|
fn comfy_combo_input(options: &[String], fallback: &str) -> Value {
|
||||||
let choices = if options.is_empty() {
|
let choices = if options.is_empty() {
|
||||||
vec![fallback.to_string()]
|
vec![fallback.to_string()]
|
||||||
@@ -850,7 +560,7 @@ fn native_model_folder_payload_or_fallback(value: Value) -> Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model_payload_items_from_names(names: Vec<String>, path_index: i64) -> Vec<Value> {
|
pub(super) fn model_payload_items_from_names(names: Vec<String>, path_index: i64) -> Vec<Value> {
|
||||||
names
|
names
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|name| json!({ "name": name, "pathIndex": path_index }))
|
.map(|name| json!({ "name": name, "pathIndex": path_index }))
|
||||||
@@ -5537,6 +5247,7 @@ pub(super) fn parse_workflow_cookie(raw: &str) -> Option<&str> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use super::models::workflow_folder_models;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_model_folder_does_not_use_route_video_models() {
|
fn video_model_folder_does_not_use_route_video_models() {
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
use super::model_payload_items_from_names;
|
||||||
|
use crate::error::AppResult;
|
||||||
|
use crate::AppState;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub(super) fn is_exact_workflow_model(value: &str) -> bool {
|
||||||
|
let normalized = value.trim();
|
||||||
|
!normalized.is_empty()
|
||||||
|
&& !normalized.to_ascii_lowercase().starts_with("re:")
|
||||||
|
&& !normalized.contains('*')
|
||||||
|
&& !normalized.contains('?')
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_image_workflow_model(value: &str) -> bool {
|
||||||
|
if !is_exact_workflow_model(value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized = value.trim().to_ascii_lowercase();
|
||||||
|
let non_image_keywords = [
|
||||||
|
"tts",
|
||||||
|
"voice",
|
||||||
|
"audio",
|
||||||
|
"embedding",
|
||||||
|
"embed",
|
||||||
|
"seedance",
|
||||||
|
"wan2",
|
||||||
|
"t2v",
|
||||||
|
"i2v",
|
||||||
|
"flf2v",
|
||||||
|
"seaweed",
|
||||||
|
"3d",
|
||||||
|
"hitem3d",
|
||||||
|
"hyper3d",
|
||||||
|
];
|
||||||
|
if non_image_keywords
|
||||||
|
.iter()
|
||||||
|
.any(|keyword| normalized.contains(keyword))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let image_keywords = [
|
||||||
|
"gpt-image",
|
||||||
|
"dall-e",
|
||||||
|
"dalle",
|
||||||
|
"t2i",
|
||||||
|
"i2i",
|
||||||
|
"midjourney",
|
||||||
|
"recraft",
|
||||||
|
"kolors",
|
||||||
|
"cogview",
|
||||||
|
"seedream",
|
||||||
|
"sdxl",
|
||||||
|
"stable-diffusion",
|
||||||
|
"stablediffusion",
|
||||||
|
"pixart",
|
||||||
|
"playground",
|
||||||
|
"firefly",
|
||||||
|
"imagen",
|
||||||
|
"flux",
|
||||||
|
"image",
|
||||||
|
"img",
|
||||||
|
"canvas",
|
||||||
|
"painting",
|
||||||
|
"draw",
|
||||||
|
];
|
||||||
|
image_keywords
|
||||||
|
.iter()
|
||||||
|
.any(|keyword| normalized.contains(keyword))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_video_workflow_model(value: &str) -> bool {
|
||||||
|
let normalized = value.trim().to_ascii_lowercase();
|
||||||
|
[
|
||||||
|
"seedance", "wan2", "t2v", "i2v", "flf2v", "video", "seaweed",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.any(|keyword| normalized.contains(keyword))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_audio_workflow_model(value: &str) -> bool {
|
||||||
|
let normalized = value.trim().to_ascii_lowercase();
|
||||||
|
["tts", "voice", "audio", "speech"]
|
||||||
|
.iter()
|
||||||
|
.any(|keyword| normalized.contains(keyword))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_embedding_workflow_model(value: &str) -> bool {
|
||||||
|
let normalized = value.trim().to_ascii_lowercase();
|
||||||
|
normalized.contains("embedding") || normalized.contains("embed")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_3d_workflow_model(value: &str) -> bool {
|
||||||
|
let normalized = value.trim().to_ascii_lowercase();
|
||||||
|
["3d", "hitem3d", "hyper3d"]
|
||||||
|
.iter()
|
||||||
|
.any(|keyword| normalized.contains(keyword))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_vision_workflow_model(value: &str) -> bool {
|
||||||
|
let normalized = value.trim().to_ascii_lowercase();
|
||||||
|
["vision", "vl", "omni", "ui-tars"]
|
||||||
|
.iter()
|
||||||
|
.any(|keyword| normalized.contains(keyword))
|
||||||
|
&& !is_image_workflow_model(value)
|
||||||
|
&& !is_video_workflow_model(value)
|
||||||
|
&& !is_embedding_workflow_model(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_chat_workflow_model(value: &str) -> bool {
|
||||||
|
is_exact_workflow_model(value)
|
||||||
|
&& !is_image_workflow_model(value)
|
||||||
|
&& !is_video_workflow_model(value)
|
||||||
|
&& !is_audio_workflow_model(value)
|
||||||
|
&& !is_embedding_workflow_model(value)
|
||||||
|
&& !is_3d_workflow_model(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn workflow_route_models(state: &AppState) -> AppResult<Vec<String>> {
|
||||||
|
let rows = sqlx::query_scalar::<_, String>(
|
||||||
|
"SELECT DISTINCT model_pattern
|
||||||
|
FROM token_routes
|
||||||
|
WHERE COALESCE(enabled, 1) = 1
|
||||||
|
AND model_pattern IS NOT NULL
|
||||||
|
AND model_pattern <> ''
|
||||||
|
ORDER BY model_pattern COLLATE NOCASE",
|
||||||
|
)
|
||||||
|
.fetch_all(&*state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
Ok(rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| value.trim().to_string())
|
||||||
|
.filter(|value| is_exact_workflow_model(value))
|
||||||
|
.filter(|value| seen.insert(value.clone()))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn image_model_choices(models: &[String]) -> Vec<String> {
|
||||||
|
let image_models = models
|
||||||
|
.iter()
|
||||||
|
.filter(|model| is_image_workflow_model(model))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if image_models.is_empty() {
|
||||||
|
models.to_vec()
|
||||||
|
} else {
|
||||||
|
image_models
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn workflow_folder_models(folder: &str, models: &[String]) -> Vec<String> {
|
||||||
|
let normalized = folder.trim().to_ascii_lowercase();
|
||||||
|
match normalized.as_str() {
|
||||||
|
"boosapi_models" | "boosapi" | "api_models" => models.to_vec(),
|
||||||
|
"image_models" => image_model_choices(models),
|
||||||
|
"chat_models" => models
|
||||||
|
.iter()
|
||||||
|
.filter(|model| is_chat_workflow_model(model))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
"clip" => models.to_vec(),
|
||||||
|
// Comfy's video_models folder is a native Comfy API model folder. Keep it
|
||||||
|
// separate from BoosAPI route models so video apps submit real Comfy API
|
||||||
|
// model names instead of OpenAI-compatible route aliases.
|
||||||
|
"video_models" => Vec::new(),
|
||||||
|
"audio_tts_models" => models
|
||||||
|
.iter()
|
||||||
|
.filter(|model| is_audio_workflow_model(model))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
"embeddings" => models
|
||||||
|
.iter()
|
||||||
|
.filter(|model| is_embedding_workflow_model(model))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
"3d_models" => models
|
||||||
|
.iter()
|
||||||
|
.filter(|model| is_3d_workflow_model(model))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
"vision_models" => models
|
||||||
|
.iter()
|
||||||
|
.filter(|model| is_vision_workflow_model(model))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn workflow_folder_is_api_only(folder: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
folder.trim().to_ascii_lowercase().as_str(),
|
||||||
|
"boosapi" | "boosapi_models" | "api_models"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn workflow_folder_has_route_models(folder: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
folder.trim().to_ascii_lowercase().as_str(),
|
||||||
|
"boosapi"
|
||||||
|
| "boosapi_models"
|
||||||
|
| "api_models"
|
||||||
|
| "image_models"
|
||||||
|
| "chat_models"
|
||||||
|
| "audio_tts_models"
|
||||||
|
| "embeddings"
|
||||||
|
| "3d_models"
|
||||||
|
| "vision_models"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn workflow_folder_route_model_items(folder: &str, route_models: &[String]) -> Vec<Value> {
|
||||||
|
model_payload_items_from_names(workflow_folder_models(folder, route_models), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn combo_choices_from_node_input(
|
||||||
|
object_info: &Value,
|
||||||
|
node_type: &str,
|
||||||
|
input_name: &str,
|
||||||
|
) -> Vec<String> {
|
||||||
|
object_info
|
||||||
|
.get(node_type)
|
||||||
|
.and_then(|node| {
|
||||||
|
node.pointer(&format!("/input/required/{input_name}"))
|
||||||
|
.or_else(|| node.pointer(&format!("/input/optional/{input_name}")))
|
||||||
|
})
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.and_then(|input| input.first())
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.map(|choices| {
|
||||||
|
choices
|
||||||
|
.iter()
|
||||||
|
.filter_map(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty() && *value != "None")
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn combo_fields_from_node(object_info: &Value, node_type: &str) -> Vec<(String, Vec<String>)> {
|
||||||
|
let mut fields = Vec::new();
|
||||||
|
let Some(node) = object_info.get(node_type) else {
|
||||||
|
return fields;
|
||||||
|
};
|
||||||
|
for section in ["required", "optional"] {
|
||||||
|
if let Some(inputs) = node
|
||||||
|
.pointer(&format!("/input/{section}"))
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
{
|
||||||
|
for (name, value) in inputs {
|
||||||
|
let Some(choices) = value
|
||||||
|
.as_array()
|
||||||
|
.and_then(|input| input.first())
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let choices = choices
|
||||||
|
.iter()
|
||||||
|
.filter_map(Value::as_str)
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty() && *value != "None")
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
fields.push((name.clone(), choices));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn native_folder_models_from_object_info(folder: &str, object_info: &Value) -> Vec<String> {
|
||||||
|
let normalized = folder.trim().to_ascii_lowercase();
|
||||||
|
match normalized.as_str() {
|
||||||
|
"checkpoints" | "image_models" => {
|
||||||
|
combo_choices_from_node_input(object_info, "CheckpointLoaderSimple", "ckpt_name")
|
||||||
|
}
|
||||||
|
"loras" => combo_choices_from_node_input(object_info, "LoraLoader", "lora_name"),
|
||||||
|
"vae" => combo_choices_from_node_input(object_info, "VAELoader", "vae_name"),
|
||||||
|
"clip" | "clip_models" | "text_encoders" => {
|
||||||
|
combo_choices_from_node_input(object_info, "CLIPLoader", "clip_name")
|
||||||
|
}
|
||||||
|
"unet" | "diffusion_models" | "diffusion_models_files" => {
|
||||||
|
combo_choices_from_node_input(object_info, "UNETLoader", "unet_name")
|
||||||
|
}
|
||||||
|
"controlnet" | "controlnets" => {
|
||||||
|
combo_choices_from_node_input(object_info, "ControlNetLoader", "control_net_name")
|
||||||
|
}
|
||||||
|
"upscale_models" => {
|
||||||
|
combo_choices_from_node_input(object_info, "UpscaleModelLoader", "model_name")
|
||||||
|
}
|
||||||
|
"embeddings" => combo_choices_from_node_input(object_info, "EmbeddingPicker", "embedding"),
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user