Normalize MoneyPrinter Comfy workflow templates
This commit is contained in:
+201
-14
@@ -493,11 +493,197 @@ fn comfy_mapping_from_body(body: &Value) -> Value {
|
|||||||
.unwrap_or_else(default_comfy_mapping)
|
.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 {
|
fn workflow_prompt_from_graph(graph: &Value) -> Value {
|
||||||
graph.get("prompt")
|
graph.get("prompt")
|
||||||
.cloned()
|
.cloned()
|
||||||
.or_else(|| graph.get("workflow").cloned())
|
.or_else(|| graph.get("workflow").cloned())
|
||||||
.or_else(|| graph.get("comfyPrompt").cloned())
|
.or_else(|| graph.get("comfyPrompt").cloned())
|
||||||
|
.or_else(|| graph_to_comfy_prompt(graph))
|
||||||
.unwrap_or_else(|| graph.clone())
|
.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 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 video_prompt = inject_comfy_video_inputs(&mut prompt, &mapping, body, plan)?;
|
||||||
let (prompt_id, response) = submit_comfy_prompt(state, prompt).await?;
|
let (prompt_id, response) = submit_comfy_prompt(state, prompt).await?;
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
@@ -957,19 +1146,17 @@ async fn create_job(
|
|||||||
if !outputs.is_empty() {
|
if !outputs.is_empty() {
|
||||||
update_progress(&state_for_task, &task_id_for_spawn, 5, "Comfy 视频完成,正在归档").await?;
|
update_progress(&state_for_task, &task_id_for_spawn, 5, "Comfy 视频完成,正在归档").await?;
|
||||||
for output in &outputs {
|
for output in &outputs {
|
||||||
if output_kind(output) == "video" {
|
if let Some(asset) = archive_comfy_output(
|
||||||
if let Some(asset) = archive_comfy_output(
|
&state_for_task,
|
||||||
&state_for_task,
|
&auth_for_task,
|
||||||
&auth_for_task,
|
output,
|
||||||
output,
|
&prompt_id,
|
||||||
&prompt_id,
|
&prompt,
|
||||||
&prompt,
|
&title,
|
||||||
&title,
|
)
|
||||||
)
|
.await
|
||||||
.await
|
{
|
||||||
{
|
assets.push(asset);
|
||||||
assets.push(asset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final_history = history;
|
final_history = history;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function planValue(job: MoneyPrinterJob, key: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function workflowPromptFromGraph(graph: any) {
|
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) {
|
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) {
|
function templateLabel(workflow: WorkflowSummary) {
|
||||||
if (workflow.isTemplate || workflow.visibility === 'template') return '官方模板';
|
if (workflow.isTemplate || workflow.visibility === 'template') return '官方模板';
|
||||||
if (workflow.visibility === 'public') return '共享模板';
|
if (workflow.visibility === 'public') return '共享模板';
|
||||||
|
|||||||
Reference in New Issue
Block a user