Files
OmniRoute/open-sse/utils/comfyuiClient.ts
duongvdo 4a779dfe3c feat: add new providers & modalities (TTS, STT, Image, Video, Music)
Add 6 TTS providers (Nvidia NIM, ElevenLabs, HuggingFace, Coqui, Tortoise,
Qwen3), 3 STT providers (Nvidia NIM, HuggingFace, Qwen3), 2 local image
providers (SD WebUI, ComfyUI), and two new modalities — Text-to-Video
(/v1/videos/generations) and Text-to-Music (/v1/music/generations).

Key design decisions:
- Format-based unified providers: local providers grouped by API format
  (comfyui, sdwebui, coqui, tortoise, openai-compatible) with configurable
  base URLs and expandable model lists
- Cloud providers kept separate (unique auth and API shapes)
- Local providers use authType: "none" — credential checks bypassed at both
  route and handler level
- Shared ComfyUI client (comfyuiClient.ts) reused across image/video/music
- Shared registry utilities (registryUtils.ts) for model parsing and listing
- Qwen3 TTS/ASR use format: "openai" — no custom handler needed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:31:29 +07:00

104 lines
2.7 KiB
TypeScript

/**
* Shared ComfyUI API Client
*
* Used by image, video, and music handlers to submit workflows,
* poll for completion, and fetch output files from a ComfyUI server.
*/
/**
* Submit a workflow to ComfyUI for execution.
* @returns The prompt_id for polling
*/
export async function submitComfyWorkflow(
baseUrl: string,
workflow: object
): Promise<string> {
const res = await fetch(`${baseUrl}/prompt`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: workflow }),
});
if (!res.ok) {
const errText = await res.text();
throw new Error(`ComfyUI submit failed (${res.status}): ${errText}`);
}
const data = await res.json();
return data.prompt_id;
}
/**
* Poll ComfyUI history endpoint until the prompt completes or times out.
* @returns The history entry for the completed prompt
*/
export async function pollComfyResult(
baseUrl: string,
promptId: string,
timeoutMs: number = 120_000
): Promise<any> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
await new Promise((r) => setTimeout(r, 2000));
const res = await fetch(`${baseUrl}/history/${promptId}`);
if (!res.ok) continue;
const data = await res.json();
const entry = data[promptId];
if (entry && entry.outputs && Object.keys(entry.outputs).length > 0) {
return entry;
}
}
throw new Error(`ComfyUI prompt ${promptId} timed out after ${timeoutMs}ms`);
}
/**
* Fetch an output file from ComfyUI.
* @returns The file contents as ArrayBuffer
*/
export async function fetchComfyOutput(
baseUrl: string,
filename: string,
subfolder: string,
type: string
): Promise<ArrayBuffer> {
const url = new URL(`${baseUrl}/view`);
url.searchParams.set("filename", filename);
url.searchParams.set("subfolder", subfolder);
url.searchParams.set("type", type);
const res = await fetch(url.toString());
if (!res.ok) {
throw new Error(`ComfyUI fetch output failed (${res.status})`);
}
return res.arrayBuffer();
}
/**
* Extract output files from a ComfyUI history entry.
* Returns an array of { filename, subfolder, type } for each output.
*/
export function extractComfyOutputFiles(
historyEntry: any
): Array<{ filename: string; subfolder: string; type: string }> {
const files: Array<{ filename: string; subfolder: string; type: string }> = [];
for (const nodeOutput of Object.values(historyEntry.outputs || {})) {
const outputs = (nodeOutput as any).images || (nodeOutput as any).gifs || (nodeOutput as any).audio || [];
for (const file of outputs) {
files.push({
filename: file.filename,
subfolder: file.subfolder || "",
type: file.type || "output",
});
}
}
return files;
}