diffusers and ComfyUI

Serving Local Models with diffusers and ComfyUI

To call a local model from a page, use a backend like Key-Safe Backend's: load the pipeline once, queue requests (a small machine runs one generation at a time) and report progress from the pipeline's callback_on_step_end hook.

ComfyUI 135,250 (github.com/Comfy-Org/ComfyUI (https://github.com/Comfy-Org/ComfyUI 135,250 ), GPL-3.0, 135,000 stars, v0.37.0 of 21 September 2026) is the other standard runtime: a node-graph editor whose boxes are pipeline stages (load checkpoint, encode prompt, sample, decode). It runs SD, SDXL 1,113 , FLUX 57,408 and more on NVIDIA, AMD, Intel and Apple GPUs or the CPU, and doubles as a server: POST a graph exported in API format to /prompt, follow the /ws WebSocket, and fetch the image through /history/{prompt_id} and /view. Choose diffusers 34,620 to embed one model in your code, and ComfyUI when artists design the workflow and your code only runs it.

A ComfyUI-style node graph executing stage by stage, fed by a queue that runs one generation at a timeHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; background: #fafaf7; font: 11px system-ui, sans-serif; color: #263238; }
  svg { width: 100%; max-width: 600px; display: block; background: #fff; }
  g.node { cursor: grab; }
</style>
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<p><button id="add">Queue a request (POST /prompt)</button> · drag the nodes · <span id="q"></span></p>
<svg viewBox="0 0 600 260" font-size="11"></svg>
<script>
  const nodes = [
    { id: 'ckpt', label: 'Load Checkpoint', sub: 'sd_v1-5.safetensors', x: 20, y: 30 },
    { id: 'pos', label: 'CLIP Text Encode', sub: 'harbor at dusk…', x: 200, y: 10 },
    { id: 'neg', label: 'CLIP Text Encode', sub: 'text, blurry', x: 200, y: 80 },
    { id: 'latent', label: 'Empty Latent', sub: '512 × 512', x: 200, y: 150 },
    { id: 'sampler', label: 'KSampler', sub: '20 steps, seed 7', x: 380, y: 60 },
    { id: 'vae', label: 'VAE Decode', sub: 'latent → pixels', x: 380, y: 150 },
    { id: 'save', label: 'Save Image', sub: '/view?filename=…', x: 470, y: 210 },
  ];
  const links = [['ckpt', 'pos'], ['ckpt', 'neg'], ['ckpt', 'sampler'], ['pos', 'sampler'], ['neg', 'sampler'], ['latent', 'sampler'], ['sampler', 'vae'], ['ckpt', 'vae'], ['vae', 'save']];
  const order = ['ckpt', 'pos', 'neg', 'latent', 'sampler', 'vae', 'save'];
  const byId = Object.fromEntries(nodes.map(n => [n.id, n]));
  const svg = d3.select('svg');
  const link = svg.append('g').selectAll('path').data(links).join('path').attr('fill', 'none').attr('stroke', '#b0bec5').attr('stroke-width', 2);
  const node = svg.append('g').selectAll('g').data(nodes).join('g').attr('class', 'node');
  node.append('rect').attr('width', 120).attr('height', 40).attr('rx', 6);
  node.append('text').attr('x', 8).attr('y', 16).attr('font-weight', 'bold').text(n => n.label);
  node.append('text').attr('x', 8).attr('y', 31).attr('fill', '#546e7a').text(n => n.sub);
  function layout() {
    node.attr('transform', n => `translate(${n.x},${n.y})`);
    link.attr('d', ([a, b]) => { const s = byId[a], t = byId[b];
      return `M${s.x + 120},${s.y + 20} C${s.x + 160},${s.y + 20} ${t.x - 40},${t.y + 20} ${t.x},${t.y + 20}`; });
  }
  node.call(d3.drag().on('drag', (e, n) => { n.x = Math.max(0, Math.min(480, e.x - 60));  n.y = Math.max(0, Math.min(220, e.y - 20));  layout(); }));
  layout();

  // A queue: a small machine runs one generation at a time; progress comes from the step callback
  const queue = [];
  let running = null, stage = 0, requestId = 0;
  function paint() {
    node.select('rect').attr('fill', n => running && order.indexOf(n.id) === stage ? '#fff3e0' : running && order.indexOf(n.id) < stage ? '#d8eee2' : '#f5f7f8')
      .attr('stroke', n => running && order.indexOf(n.id) === stage ? '#e09a10' : '#90a4ae');
    d3.select('#q').text(running ? `running #${running} (${order[stage]})` + (queue.length ? `, ${queue.length} waiting` : '') : 'idle');
  }
  function next() {
    if (running) return;
    running = queue.shift();
    if (!running) { paint();  return; }
    stage = 0;
    const timer = d3.interval(() => {
      stage++;
      if (stage >= order.length) { timer.stop();  running = null;  next(); }
      paint();
    }, 500);
    paint();
  }
  d3.select('#add').on('click', () => { queue.push(++requestId);  next(); });
  queue.push(++requestId, ++requestId);
  next();
</script>