Normalize MoneyPrinter Comfy workflow templates
This commit is contained in:
+190
-3
@@ -493,11 +493,197 @@ fn comfy_mapping_from_body(body: &Value) -> Value {
|
||||
.unwrap_or_else(default_comfy_mapping)
|
||||
}
|
||||
|
||||
fn mapping_has_node(mapping: &Value, keys: &[&str]) -> bool {
|
||||
mapping_node_id(mapping, keys).is_some_and(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
fn graph_node_type(node: &Value) -> String {
|
||||
node.get("type")
|
||||
.or_else(|| node.get("class_type"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn graph_node_params(node: &Value) -> serde_json::Map<String, Value> {
|
||||
node.get("params")
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.or_else(|| node.get("inputs").and_then(Value::as_object).cloned())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn normalize_comfy_value(class_type: &str, field: &str, value: Value) -> Value {
|
||||
match (class_type, field) {
|
||||
("KSampler", "cfg") => value.as_f64().filter(|v| (0.0..=50.0).contains(v)).map_or(json!(8.0), |v| json!(v)),
|
||||
("KSampler", "denoise") => value.as_f64().map(|v| if v > 1.0 { v / 100.0 } else { v }).filter(|v| (0.0..=1.0).contains(v)).map_or(json!(1.0), |v| json!(v)),
|
||||
("KSampler", "sampler_name") => {
|
||||
if value.is_string() { value } else { json!("euler") }
|
||||
}
|
||||
("KSampler", "scheduler") => {
|
||||
if value.is_string() { value } else { json!("normal") }
|
||||
}
|
||||
("KSampler", "seed") => value.as_i64().map_or(json!(0), |v| json!(v.max(0))),
|
||||
("EmptyLatentImage", "width") | ("EmptyLatentImage", "height") => {
|
||||
value.as_i64().filter(|v| (64..=4096).contains(v)).map_or(json!(1024), |v| json!(v))
|
||||
}
|
||||
("EmptyLatentImage", "batch_size") => {
|
||||
value.as_i64().filter(|v| (1..=16).contains(v)).map_or(json!(1), |v| json!(v))
|
||||
}
|
||||
_ => value,
|
||||
}
|
||||
}
|
||||
|
||||
fn split_endpoint(value: &str) -> Option<(&str, &str)> {
|
||||
let (node_id, port) = value.split_once('.')?;
|
||||
Some((node_id.trim(), port.trim()))
|
||||
}
|
||||
|
||||
fn graph_to_comfy_prompt(graph: &Value) -> Option<Value> {
|
||||
let nodes = graph.get("nodes")?.as_array()?;
|
||||
let mut prompt = serde_json::Map::new();
|
||||
for node in nodes {
|
||||
let id = scalar_string(node.get("id"))?;
|
||||
let class_type = graph_node_type(node);
|
||||
if class_type.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut inputs = serde_json::Map::new();
|
||||
for (field, value) in graph_node_params(node) {
|
||||
inputs.insert(field.clone(), normalize_comfy_value(&class_type, &field, value));
|
||||
}
|
||||
prompt.insert(id, json!({
|
||||
"class_type": class_type,
|
||||
"inputs": inputs,
|
||||
}));
|
||||
}
|
||||
for edge in graph.get("edges").and_then(Value::as_array).into_iter().flatten() {
|
||||
let Some(from) = edge.get("from").and_then(Value::as_str) else { continue; };
|
||||
let Some(to) = edge.get("to").and_then(Value::as_str) else { continue; };
|
||||
let Some((from_id, from_port)) = split_endpoint(from) else { continue; };
|
||||
let Some((to_id, to_field)) = split_endpoint(to) else { continue; };
|
||||
if let Some(inputs) = prompt
|
||||
.get_mut(to_id)
|
||||
.and_then(Value::as_object_mut)
|
||||
.and_then(|node| node.get_mut("inputs"))
|
||||
.and_then(Value::as_object_mut)
|
||||
{
|
||||
inputs.insert(to_field.to_string(), json!([from_id, 0]));
|
||||
if to_field == "model" && from_port == "MODEL" {
|
||||
inputs.insert("model".to_string(), json!([from_id, 0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
repair_common_comfy_prompt_links(&mut prompt);
|
||||
Some(Value::Object(prompt))
|
||||
}
|
||||
|
||||
fn first_node_by_class(prompt: &serde_json::Map<String, Value>, class_type: &str) -> Option<String> {
|
||||
prompt
|
||||
.iter()
|
||||
.find(|(_, node)| node.get("class_type").and_then(Value::as_str) == Some(class_type))
|
||||
.map(|(id, _)| id.clone())
|
||||
}
|
||||
|
||||
fn infer_comfy_mapping(prompt: &Value) -> Value {
|
||||
let Some(object) = prompt.as_object() else {
|
||||
return default_comfy_mapping();
|
||||
};
|
||||
let text_nodes = clip_text_nodes(object);
|
||||
let prompt_node = text_nodes
|
||||
.first()
|
||||
.cloned()
|
||||
.or_else(|| first_node_by_class(object, "TextGenerateLTX2Prompt"))
|
||||
.or_else(|| first_node_by_class(object, "Wan2ImageToVideoApi"))
|
||||
.unwrap_or_default();
|
||||
let negative_node = text_nodes.get(1).cloned().unwrap_or_default();
|
||||
let video_node = first_node_by_class(object, "Wan2ImageToVideoApi")
|
||||
.or_else(|| first_node_by_class(object, "TextGenerateLTX2Prompt"))
|
||||
.or_else(|| first_node_by_class(object, "KSampler"))
|
||||
.or_else(|| first_node_by_class(object, "EmptyLatentImage"))
|
||||
.unwrap_or_default();
|
||||
json!({
|
||||
"prompt": {
|
||||
"nodeId": prompt_node,
|
||||
"field": "text"
|
||||
},
|
||||
"negativePrompt": {
|
||||
"nodeId": negative_node,
|
||||
"field": "text"
|
||||
},
|
||||
"ratio": {
|
||||
"nodeId": video_node,
|
||||
"field": "ratio"
|
||||
},
|
||||
"resolution": {
|
||||
"nodeId": video_node,
|
||||
"field": "resolution"
|
||||
},
|
||||
"duration": {
|
||||
"nodeId": video_node,
|
||||
"field": "duration"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn clip_text_nodes(prompt: &serde_json::Map<String, Value>) -> Vec<String> {
|
||||
prompt
|
||||
.iter()
|
||||
.filter(|(_, node)| node.get("class_type").and_then(Value::as_str) == Some("CLIPTextEncode"))
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn repair_common_comfy_prompt_links(prompt: &mut serde_json::Map<String, Value>) {
|
||||
let checkpoint = first_node_by_class(prompt, "CheckpointLoaderSimple");
|
||||
let latent = first_node_by_class(prompt, "EmptyLatentImage");
|
||||
let ksampler = first_node_by_class(prompt, "KSampler");
|
||||
let text_nodes = clip_text_nodes(prompt);
|
||||
if let Some(ksampler_id) = ksampler {
|
||||
if let Some(inputs) = prompt
|
||||
.get_mut(&ksampler_id)
|
||||
.and_then(Value::as_object_mut)
|
||||
.and_then(|node| node.get_mut("inputs"))
|
||||
.and_then(Value::as_object_mut)
|
||||
{
|
||||
if let Some(checkpoint_id) = checkpoint.as_deref() {
|
||||
inputs.insert("model".to_string(), json!([checkpoint_id, 0]));
|
||||
}
|
||||
if let Some(latent_id) = latent.as_deref() {
|
||||
inputs.insert("latent_image".to_string(), json!([latent_id, 0]));
|
||||
}
|
||||
if let Some(positive_id) = text_nodes.first() {
|
||||
inputs.insert("positive".to_string(), json!([positive_id, 0]));
|
||||
}
|
||||
if let Some(negative_id) = text_nodes.get(1).or_else(|| text_nodes.first()) {
|
||||
inputs.insert("negative".to_string(), json!([negative_id, 0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(vae_decode_id) = first_node_by_class(prompt, "VAEDecode") {
|
||||
let ksampler_for_vae = first_node_by_class(prompt, "KSampler");
|
||||
if let Some(inputs) = prompt
|
||||
.get_mut(&vae_decode_id)
|
||||
.and_then(Value::as_object_mut)
|
||||
.and_then(|node| node.get_mut("inputs"))
|
||||
.and_then(Value::as_object_mut)
|
||||
{
|
||||
if let Some(ksampler_id) = ksampler_for_vae {
|
||||
inputs.insert("samples".to_string(), json!([ksampler_id, 0]));
|
||||
}
|
||||
if let Some(checkpoint_id) = checkpoint.as_deref() {
|
||||
inputs.insert("vae".to_string(), json!([checkpoint_id, 2]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn workflow_prompt_from_graph(graph: &Value) -> Value {
|
||||
graph.get("prompt")
|
||||
.cloned()
|
||||
.or_else(|| graph.get("workflow").cloned())
|
||||
.or_else(|| graph.get("comfyPrompt").cloned())
|
||||
.or_else(|| graph_to_comfy_prompt(graph))
|
||||
.unwrap_or_else(|| graph.clone())
|
||||
}
|
||||
|
||||
@@ -720,7 +906,10 @@ async fn submit_comfy_video(
|
||||
));
|
||||
}
|
||||
let mut prompt = workflow_prompt_from_body(state, auth, body).await?;
|
||||
let mapping = comfy_mapping_from_body(body);
|
||||
let mut mapping = comfy_mapping_from_body(body);
|
||||
if !mapping_has_node(&mapping, &["prompt", "positive", "text", "promptNode"]) {
|
||||
mapping = infer_comfy_mapping(&prompt);
|
||||
}
|
||||
let video_prompt = inject_comfy_video_inputs(&mut prompt, &mapping, body, plan)?;
|
||||
let (prompt_id, response) = submit_comfy_prompt(state, prompt).await?;
|
||||
Ok(json!({
|
||||
@@ -957,7 +1146,6 @@ async fn create_job(
|
||||
if !outputs.is_empty() {
|
||||
update_progress(&state_for_task, &task_id_for_spawn, 5, "Comfy 视频完成,正在归档").await?;
|
||||
for output in &outputs {
|
||||
if output_kind(output) == "video" {
|
||||
if let Some(asset) = archive_comfy_output(
|
||||
&state_for_task,
|
||||
&auth_for_task,
|
||||
@@ -971,7 +1159,6 @@ async fn create_job(
|
||||
assets.push(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
final_history = history;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ function planValue(job: MoneyPrinterJob, key: string) {
|
||||
}
|
||||
|
||||
function workflowPromptFromGraph(graph: any) {
|
||||
return graph?.prompt || graph?.workflow || graph?.comfyPrompt || graph || null;
|
||||
return graph?.prompt || graph?.workflow || graph?.comfyPrompt || graphToComfyPrompt(graph) || graph || null;
|
||||
}
|
||||
|
||||
function mappingFromGraph(graph: any) {
|
||||
@@ -74,6 +74,72 @@ function inferMappingFromPrompt(prompt: any) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeParam(classType: string, field: string, value: any) {
|
||||
if (classType === 'KSampler' && field === 'cfg') {
|
||||
return typeof value === 'number' && value >= 0 && value <= 50 ? value : 8;
|
||||
}
|
||||
if (classType === 'KSampler' && field === 'denoise') {
|
||||
const numeric = typeof value === 'number' ? value : Number(value);
|
||||
if (!Number.isFinite(numeric)) return 1;
|
||||
return numeric > 1 ? Math.min(1, numeric / 100) : Math.max(0, numeric);
|
||||
}
|
||||
if (classType === 'KSampler' && field === 'sampler_name') return typeof value === 'string' ? value : 'euler';
|
||||
if (classType === 'KSampler' && field === 'scheduler') return typeof value === 'string' ? value : 'normal';
|
||||
if (classType === 'KSampler' && field === 'seed') return Number.isFinite(Number(value)) ? Math.max(0, Number(value)) : 0;
|
||||
if (classType === 'EmptyLatentImage' && ['width', 'height'].includes(field)) {
|
||||
const numeric = Number(value);
|
||||
return numeric >= 64 && numeric <= 4096 ? numeric : 1024;
|
||||
}
|
||||
if (classType === 'EmptyLatentImage' && field === 'batch_size') {
|
||||
const numeric = Number(value);
|
||||
return numeric >= 1 && numeric <= 16 ? numeric : 1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function graphToComfyPrompt(graph: any) {
|
||||
if (!Array.isArray(graph?.nodes)) return null;
|
||||
const prompt: Record<string, any> = {};
|
||||
for (const node of graph.nodes) {
|
||||
const id = String(node?.id || '');
|
||||
const classType = String(node?.type || node?.class_type || '');
|
||||
if (!id || !classType) continue;
|
||||
const params = node?.params && typeof node.params === 'object' ? node.params : {};
|
||||
const inputs: Record<string, any> = {};
|
||||
for (const [field, value] of Object.entries(params)) {
|
||||
inputs[field] = normalizeParam(classType, field, value);
|
||||
}
|
||||
prompt[id] = { class_type: classType, inputs };
|
||||
}
|
||||
for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) {
|
||||
const from = String(edge?.from || '');
|
||||
const to = String(edge?.to || '');
|
||||
const [fromId] = from.split('.');
|
||||
const [toId, toField] = to.split('.');
|
||||
if (fromId && toId && toField && prompt[toId]?.inputs) {
|
||||
prompt[toId].inputs[toField] = [fromId, 0];
|
||||
}
|
||||
}
|
||||
const firstByType = (type: string) => Object.entries(prompt).find(([, node]) => node.class_type === type)?.[0] || '';
|
||||
const allByType = (type: string) => Object.entries(prompt).filter(([, node]) => node.class_type === type).map(([id]) => id);
|
||||
const checkpoint = firstByType('CheckpointLoaderSimple');
|
||||
const latent = firstByType('EmptyLatentImage');
|
||||
const ksampler = firstByType('KSampler');
|
||||
const textNodes = allByType('CLIPTextEncode');
|
||||
if (ksampler) {
|
||||
if (checkpoint) prompt[ksampler].inputs.model = [checkpoint, 0];
|
||||
if (latent) prompt[ksampler].inputs.latent_image = [latent, 0];
|
||||
if (textNodes[0]) prompt[ksampler].inputs.positive = [textNodes[0], 0];
|
||||
if (textNodes[1] || textNodes[0]) prompt[ksampler].inputs.negative = [textNodes[1] || textNodes[0], 0];
|
||||
}
|
||||
const vaeDecode = firstByType('VAEDecode');
|
||||
if (vaeDecode) {
|
||||
if (ksampler) prompt[vaeDecode].inputs.samples = [ksampler, 0];
|
||||
if (checkpoint) prompt[vaeDecode].inputs.vae = [checkpoint, 2];
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
function templateLabel(workflow: WorkflowSummary) {
|
||||
if (workflow.isTemplate || workflow.visibility === 'template') return '官方模板';
|
||||
if (workflow.visibility === 'public') return '共享模板';
|
||||
|
||||
Reference in New Issue
Block a user