Spaces:
Running
Running
File size: 6,959 Bytes
cd3f86a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
/**
* Service for interacting with local ComfyUI instances
*/
/**
* Converts a standard ComfyUI UI workflow (the one with 'nodes' array)
* to the API format that the /prompt endpoint expects.
*/
const convertUiToApi = (uiWorkflow: any): any => {
const apiPrompt: any = {};
if (!uiWorkflow.nodes || !Array.isArray(uiWorkflow.nodes)) {
return uiWorkflow; // Already in API format or unknown
}
uiWorkflow.nodes.forEach((node: any) => {
const nodeId = node.id.toString();
const inputs: any = {};
// Map inputs based on links
if (uiWorkflow.links && node.inputs) {
node.inputs.forEach((input: any) => { // Removed unused 'index' parameter
const linkId = input.link;
if (linkId) {
const link = uiWorkflow.links.find((l: any) => l[0] === linkId);
if (link) {
// link format: [id, origin_id, origin_slot, target_id, target_slot, type]
inputs[input.name] = [link[1].toString(), link[2]];
}
}
});
}
// Map widgets to inputs
// This is a simplified mapping; standard nodes usually have widgets in a specific order
if (node.widgets_values && Array.isArray(node.widgets_values)) {
// Note: This mapping is brittle as it depends on node implementation,
// but for standard nodes it often works.
// We'll primarily rely on the explicit injection logic later.
node.widgets_values.forEach((val: any, idx: number) => {
// We don't strictly know the keys here without the node definition,
// but we store them to be safe.
inputs[`_widget_${idx}`] = val;
});
}
apiPrompt[nodeId] = {
class_type: node.type,
inputs: inputs,
_meta: { title: node.title || node.type }
};
});
return apiPrompt;
};
export const sendComfyPrompt = async (
serverUrl: string,
workflow: any,
promptText: string,
seed: number,
steps: number,
useSecureBridge: boolean = false,
signal?: AbortSignal
): Promise<string> => {
const baseUrl = serverUrl.replace(/\/+$/, '');
console.log(`[ComfyUI] Starting preview. Bridge: ${useSecureBridge}, Target: ${baseUrl}`);
// 1. Prepare Workflow
let apiPrompt: any = {};
const isUiFormat = workflow.nodes && Array.isArray(workflow.nodes);
if (isUiFormat) {
console.log("[ComfyUI] Standard UI format detected. Attempting internal mapping...");
// For standard UI format, we'll try to find the nodes by type and title
apiPrompt = JSON.parse(JSON.stringify(workflow)); // Work on a copy
let promptNode = apiPrompt.nodes.find((n: any) =>
n.type === 'CLIPTextEncode' &&
((n.title || "").toLowerCase().includes("positive") || !(n.title || "").toLowerCase().includes("negative"))
);
let samplerNode = apiPrompt.nodes.find((n: any) => n.type === 'KSampler' || n.type === 'KSamplerAdvanced');
if (promptNode) {
// In UI format, prompt is usually the first widget
if (promptNode.widgets_values) promptNode.widgets_values[0] = promptText;
}
if (samplerNode && samplerNode.widgets_values) {
if (seed !== -1) samplerNode.widgets_values[0] = seed;
if (steps !== -1) samplerNode.widgets_values[2] = steps;
}
// IMPORTANT: The /prompt endpoint REQUIRES API format.
// If we have UI format, we MUST convert it or it will fail.
apiPrompt = convertUiToApi(apiPrompt);
} else {
apiPrompt = JSON.parse(JSON.stringify(workflow));
// Identify nodes in API format
let promptNodeId = '';
let samplerNodeId = '';
for (const id in apiPrompt) {
const node = apiPrompt[id];
const type = node.class_type;
const title = (node._meta?.title || "").toLowerCase();
if (!promptNodeId && type === 'CLIPTextEncode' && (title.includes('positive') || !title.includes('negative'))) promptNodeId = id;
if (!samplerNodeId && (type === 'KSampler' || type === 'KSamplerAdvanced')) samplerNodeId = id;
}
if (promptNodeId) apiPrompt[promptNodeId].inputs.text = promptText;
if (samplerNodeId) {
if (seed !== -1) apiPrompt[samplerNodeId].inputs.seed = seed;
if (steps !== -1) apiPrompt[samplerNodeId].inputs.steps = steps;
}
}
// 2. Determine Endpoint
let fetchUrl = `${baseUrl}/prompt`;
let fetchHeaders: Record<string, string> = { 'Content-Type': 'application/json' };
if (useSecureBridge) {
fetchUrl = `${window.location.origin}/comfy-bridge/prompt`;
fetchHeaders['x-bridge-target'] = baseUrl;
}
// 3. Send Request
const response = await fetch(fetchUrl, {
method: 'POST',
headers: fetchHeaders,
body: JSON.stringify({ prompt: apiPrompt }),
signal
}).catch(err => {
if (err.name === 'AbortError') throw err;
throw new Error(`Connection failed: ${err.message}. Ensure your ComfyUI server or Bridge is reachable.`);
});
if (!response.ok) {
const errText = await response.text();
throw new Error(`Server Error (${response.status}): ${errText.substring(0, 100)}...`);
}
const { prompt_id } = await response.json();
// 4. Poll
const pollUrl = useSecureBridge ? `${window.location.origin}/comfy-bridge/history/${prompt_id}` : `${baseUrl}/history/${prompt_id}`;
const pollHeaders: HeadersInit = useSecureBridge ? { 'x-bridge-target': baseUrl } : {}; // Added explicit HeadersInit type
for (let i = 0; i < 60; i++) {
if (signal?.aborted) throw new Error("Aborted");
const hRes = await fetch(pollUrl, { headers: pollHeaders, signal });
if (hRes.ok) {
const history = await hRes.json();
if (history[prompt_id]) {
const outputs = history[prompt_id].outputs;
for (const nodeId in outputs) {
if (outputs[nodeId].images?.length > 0) {
const img = outputs[nodeId].images[0];
let finalUrl = useSecureBridge
? `${window.location.origin}/comfy-bridge/view?filename=${img.filename}&subfolder=${img.subfolder}&type=${img.type}&target_base=${encodeURIComponent(baseUrl)}`
: `${baseUrl}/view?filename=${img.filename}&subfolder=${img.subfolder}&type=${img.type}`;
return finalUrl;
}
}
}
}
await new Promise(r => setTimeout(r, 3000));
}
throw new Error("Preview generation timed out.");
}; |