loracaptionertaz / services /comfyService.ts
comfyuiman's picture
Upload 20 files
cd3f86a verified
/**
* 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.");
};