Real per-step progress bars for local GPU inference

Backend:
- local_diffusion.py: add _make_step_cb() that writes step/total_steps/
  progress into _states on every diffusers callback_on_step_end; wired into
  txt2img, inpaint, img2img with TypeError fallback for older diffusers
- ai_tools.py: GET /api/generate/progress SSE endpoint — streams _states
  as JSON array every 200ms so clients get live denoising step counts

Frontend:
- progress_overlay.js: add connectProgressSSE(pipeType, baseUrl) /
  disconnectProgressSSE() — opens EventSource, maps step/total_steps
  to bar percentage (0→85% during denoising, 85→100 for decode/place)
- text_to_image.js: connect SSE before POST, disconnect on done/error
- selection_actions.js: connect SSE for AI edit / asymmetry operations

Result: for local GPU, progress bar shows "Step 12 / 30" with exact fill;
for remote providers and upscale (no step callbacks), shimmer animates.

https://claude.ai/code/session_01WVDg7amsy1TTtxvpku7bcM
This commit is contained in:
Claude
2026-06-13 16:48:55 +00:00
parent ee94282753
commit 372ab48991
5 changed files with 214 additions and 50 deletions
+38
View File
@@ -19,6 +19,44 @@ var _shimmerAnim = null;
var _fakeTimer = null;
var _currentPct = 0;
// ── SSE progress connection ───────────────────────────────────────────────────
var _sse = null;
/**
* Open an EventSource to /api/generate/progress and drive the bar with real
* denoising step counts from the local GPU pipeline.
*
* @param {string} pipeType - 'txt2img' | 'inpaint' | 'img2img'
* @param {string} baseUrl - window.API_BASE_URL or ''
*/
export function connectProgressSSE(pipeType, baseUrl) {
disconnectProgressSSE();
try {
var url = (baseUrl || '') + '/api/generate/progress';
_sse = new EventSource(url);
_sse.onmessage = (e) => {
try {
var states = JSON.parse(e.data);
var s = Array.isArray(states)
? states.find(st => st.pipeline === pipeType)
: null;
if (s && s.state === 'running' && s.total_steps) {
var pct = Math.round(s.step / s.total_steps * 85);
updateProgress(pct, s.message || `Step ${s.step} / ${s.total_steps}`);
}
} catch { /* malformed event — ignore */ }
};
_sse.onerror = () => disconnectProgressSSE();
} catch { /* SSE not supported */ }
}
export function disconnectProgressSSE() {
if (_sse) { _sse.close(); _sse = null; }
}
// ── Progress overlay ──────────────────────────────────────────────────────────
export function showProgress(message, estimatedSeconds) {
hideProgress();
@@ -12,7 +12,7 @@ import Dialog_class from './../../libs/popup.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
import apiService from './../../services/api.js';
import { getCapabilities } from './../../api/capabilities.js';
import { showProgress, updateProgress, hideProgress } from './../../libs/progress_overlay.js';
import { showProgress, updateProgress, hideProgress, connectProgressSSE, disconnectProgressSSE } from './../../libs/progress_overlay.js';
var instance = null;
@@ -142,7 +142,8 @@ class Generate_text_to_image_class {
if (this.isProcessing) return;
this.isProcessing = true;
showProgress('Generating image… this may take a minute on local GPU', estSec || 60);
connectProgressSSE('txt2img', window.API_BASE_URL || '');
showProgress('Generating image…', estSec || 60);
try {
var result = await apiService.textToImage(params.prompt, {
@@ -185,11 +186,13 @@ class Generate_text_to_image_class {
])
);
}
disconnectProgressSSE();
hideProgress();
alertify.success('Image generated!');
this.isProcessing = false;
};
img.onerror = () => {
disconnectProgressSSE();
hideProgress();
alertify.error('Failed to load generated image.');
this.isProcessing = false;
@@ -197,6 +200,7 @@ class Generate_text_to_image_class {
img.src = 'data:image/png;base64,' + result.result;
} catch (err) {
disconnectProgressSSE();
hideProgress();
alertify.error('Generation failed: ' + (err.message || err));
this.isProcessing = false;
+7 -1
View File
@@ -18,7 +18,7 @@ import app from './../app.js';
import config from './../config.js';
import Base_layers_class from './../core/base-layers.js';
import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
import { showProgress, hideProgress } from './../libs/progress_overlay.js';
import { showProgress, updateProgress, hideProgress, connectProgressSSE, disconnectProgressSSE } from './../libs/progress_overlay.js';
const BASE = window.API_BASE_URL || '';
@@ -196,6 +196,7 @@ export class SelectionActions {
async _makeAsymmetric() {
if (!this._check()) return;
this.hide();
connectProgressSSE('inpaint', window.API_BASE_URL || '');
showProgress('AI is adding natural asymmetry…', 60);
try {
var res = await _post('/api/image/ai-edit-region', {
@@ -208,9 +209,11 @@ export class SelectionActions {
});
this.tool.updateLayerWithResult(res.result);
this.tool.clearSelection();
disconnectProgressSSE();
hideProgress();
alertify.success('Made less symmetrical!');
} catch (e) {
disconnectProgressSSE();
hideProgress();
alertify.error('AI edit failed: ' + e.message);
}
@@ -219,6 +222,7 @@ export class SelectionActions {
async _aiEditRegion(instruction) {
if (!this._check()) return;
this.hide();
connectProgressSSE('inpaint', window.API_BASE_URL || '');
showProgress('AI is editing the region…', 60);
try {
var res = await _post('/api/image/ai-edit-region', {
@@ -230,9 +234,11 @@ export class SelectionActions {
});
this.tool.updateLayerWithResult(res.result);
this.tool.clearSelection();
disconnectProgressSSE();
hideProgress();
alertify.success('Done!');
} catch (e) {
disconnectProgressSSE();
hideProgress();
alertify.error('AI edit failed: ' + e.message);
}