Files
PaintPlus/frontend/src/js/modules/image/upscale.js
T
Claude ee94282753 Add progress overlay and fix txt2img for local GPU
- New progress_overlay.js: animated fullscreen overlay with shimmer bar,
  fake progress creep, Esc-to-cancel, used by all slow AI operations
- text_to_image.js: allow local_gpu provider (was incorrectly blocked);
  show provider/model/VRAM info in dialog; show estimated generation time;
  use progress overlay during generation
- upscale.js: replace alertify.message with progress overlay (90s estimate
  for AI upscale, 10s for Lanczos)
- frame_fit.js: progress overlay for extend mode (AI outpaint ~45s)
- print_prepare.js: progress overlay for full upscale+frame chain (~2 min)
- selection_actions.js: progress overlay for all AI region edits

https://claude.ai/code/session_01WVDg7amsy1TTtxvpku7bcM
2026-06-13 16:37:04 +00:00

307 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Upscale — increase image resolution.
* Fetches available methods from /api/print/upscale/available on first open.
* Auto-selects the recommended method; user can override.
* If no AI upscaler is found, polls /api/print/upscale/install-status while
* the backend auto-installs Real-ESRGAN NCNN Vulkan, then refreshes and continues.
*
* Menu target: image/upscale.upscale
*/
import app from './../../app.js';
import config from './../../config.js';
import Base_layers_class from './../../core/base-layers.js';
import Dialog_class from './../../libs/popup.js';
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
import { showProgress, updateProgress, hideProgress } from './../../libs/progress_overlay.js';
var instance = null;
const METHOD_LABELS = {
auto: 'Auto (best available)',
realesrgan_pytorch: 'Real-ESRGAN — PyTorch',
realesrgan_ncnn: 'Real-ESRGAN — NCNN Vulkan',
lanczos: 'Lanczos (fast, no AI)',
};
class Image_upscale_class {
constructor() {
if (instance) return instance;
instance = this;
this.Base_layers = new Base_layers_class();
this.Dialog = new Dialog_class();
this.isProcessing = false;
this._caps = null;
}
async upscale() {
if (!config.layer || config.layer.type !== 'image') {
alertify.error('Select an image layer first.');
return;
}
// If a previous caps fetch showed no AI upscaler, check install progress
var caps = await this._fetchCaps();
if (!caps.realesrgan_pytorch && !caps.realesrgan_ncnn) {
await this._waitForInstall(caps);
// Re-fetch caps after install
this._caps = null;
caps = await this._fetchCaps();
}
this._showDialog(caps);
}
_showDialog(caps) {
var W = config.layer.width_original;
var H = config.layer.height_original;
var available = ['auto', ...caps.methods];
var methodValues = [...new Set(available)];
var methodLabels = methodValues.map(m => {
var label = METHOD_LABELS[m] || m;
if (m === 'auto') {
label = `Auto → ${caps.recommended_label}`;
} else if (m === caps.recommended && m !== 'auto') {
label += ' ★';
}
return label;
});
var deviceNote = '';
if (caps.realesrgan_pytorch) {
var dev = caps.realesrgan_pytorch_device;
var devLabel = dev === 'cuda' ? 'CUDA GPU'
: dev === 'mps' ? 'Apple Silicon'
: 'CPU (slow — ~13 min for large images)';
deviceNote += `PyTorch: ${devLabel}. `;
}
if (caps.realesrgan_ncnn) {
deviceNote += 'NCNN Vulkan binary found. ';
}
if (!caps.realesrgan_pytorch && !caps.realesrgan_ncnn) {
var installState = (caps.ncnn_install_status || {}).state;
if (installState === 'skipped') {
deviceNote = 'Headless server — no Vulkan GPU. Lanczos only. '
+ 'Install Real-ESRGAN PyTorch for AI quality on CPU.';
} else {
deviceNote = 'No AI upscaler available — Lanczos only.';
}
}
var _this = this;
this.Dialog.show({
title: 'Upscale Image',
params: [
{
title: '',
html: `<div style="font-size:11px;color:#888;margin:0 0 8px;">
Current: ${W}×${H}px<br>
${deviceNote}
</div>`,
},
{
name: 'scale',
title: 'Scale factor:',
value: '2×',
values: ['1.5×', '2×', '3×', '4×'],
type: 'select',
},
{
name: 'method',
title: 'Method:',
value: methodLabels[0],
values: methodLabels,
type: 'select',
},
{
name: 'new_layer',
title: 'Result as new layer (keep original):',
value: false,
},
],
on_finish: async function (params) {
var labelIdx = methodLabels.indexOf(params.method);
var methodKey = labelIdx >= 0 ? methodValues[labelIdx] : 'auto';
var scale = parseFloat(params.scale);
await _this._run(scale, methodKey, params.new_layer);
},
});
}
/**
* Poll install-status until done/failed/skipped, showing a progress bar.
* On headless machines the server sets state=skipped immediately — no wait.
*/
async _waitForInstall(caps) {
var installStatus = caps.ncnn_install_status || {};
var terminalStates = ['done', 'failed', 'skipped'];
if (terminalStates.includes(installStatus.state)) {
if (installStatus.state === 'skipped') {
// Headless — just proceed, dialog will show Lanczos or PyTorch CPU
alertify.message(installStatus.message || 'No Vulkan GPU — using CPU upscaler.', 4);
}
return;
}
return new Promise((resolve) => {
alertify.message(
`<div>Installing Real-ESRGAN AI upscaler…<br>
<progress id="esrgan-install-progress" value="0" max="100"
style="width:100%;margin-top:6px;"></progress>
<span id="esrgan-install-pct">0%</span></div>`,
0
);
var poll = setInterval(async () => {
try {
var base = window.API_BASE_URL || '';
var r = await fetch(`${base}/api/print/upscale/install-status`);
if (!r.ok) return;
var s = await r.json();
var bar = document.getElementById('esrgan-install-progress');
var pct = document.getElementById('esrgan-install-pct');
if (bar) bar.value = s.progress || 0;
if (pct) pct.textContent = `${s.progress || 0}%`;
if (s.state === 'done') {
clearInterval(poll);
alertify.dismissAll();
alertify.success('Real-ESRGAN NCNN installed.');
resolve();
} else if (s.state === 'skipped') {
clearInterval(poll);
alertify.dismissAll();
alertify.message(s.message || 'No Vulkan GPU — using CPU upscaler.', 4);
resolve();
} else if (s.state === 'failed') {
clearInterval(poll);
alertify.dismissAll();
alertify.warning('AI upscaler install failed — using Lanczos.');
resolve();
}
} catch { /* network hiccup, keep polling */ }
}, 1500);
});
}
async _fetchCaps() {
if (this._caps) return this._caps;
try {
var base = window.API_BASE_URL || '';
var r = await fetch(`${base}/api/print/upscale/available`);
if (r.ok) {
this._caps = await r.json();
}
} catch { /* ignore */ }
if (!this._caps) {
this._caps = {
lanczos: true,
realesrgan_pytorch: false,
realesrgan_ncnn: false,
recommended: 'lanczos',
recommended_label: 'Lanczos',
methods: ['lanczos'],
ncnn_install_status: { state: 'idle', progress: 0 },
};
}
return this._caps;
}
async _run(scale, method, newLayer) {
if (this.isProcessing) return;
this.isProcessing = true;
var caps = this._caps || {};
var methodLabel = method === 'auto'
? `Auto (${caps.recommended_label || 'best available'})`
: (METHOD_LABELS[method] || method);
var isAI = method !== 'lanczos';
showProgress(
`Upscaling ${scale}× with ${methodLabel}…` +
(isAI ? '\nAI is reconstructing detail — this may take 30120 seconds.' : ''),
isAI ? 90 : 10
);
try {
var layerCanvas = document.createElement('canvas');
layerCanvas.width = config.layer.width_original;
layerCanvas.height = config.layer.height_original;
layerCanvas.getContext('2d').drawImage(config.layer.link, 0, 0);
var imageB64 = layerCanvas.toDataURL('image/png').split(',')[1];
var base = window.API_BASE_URL || '';
var r = await fetch(`${base}/api/print/upscale`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: imageB64, scale, method }),
});
if (!r.ok) {
var err = await r.json().catch(() => ({ detail: 'Server error' }));
throw new Error(err.detail || 'Upscale failed');
}
var result = await r.json();
var img = new Image();
img.onload = () => {
var resultCanvas = document.createElement('canvas');
resultCanvas.width = img.naturalWidth;
resultCanvas.height = img.naturalHeight;
resultCanvas.getContext('2d').drawImage(img, 0, 0);
var usedLabel = result.method.replace('realesrgan_pytorch_', 'ESRGAN/')
.replace('realesrgan_ncnn', 'ESRGAN/NCNN');
if (newLayer) {
app.State.do_action(
new app.Actions.Bundle_action('upscale_layer', 'Upscale', [
new app.Actions.Insert_layer_action({
name: `${scale}× ${usedLabel}`,
type: 'image',
data: img.src,
x: 0, y: 0,
width: img.naturalWidth,
height: img.naturalHeight,
width_original: img.naturalWidth,
height_original: img.naturalHeight,
})
])
);
} else {
app.State.do_action(
new app.Actions.Bundle_action('upscale', 'Upscale', [
new app.Actions.Update_layer_image_action(resultCanvas)
])
);
}
hideProgress();
alertify.success(
`${result.output.width}×${result.output.height}px · ${usedLabel}`
);
this.isProcessing = false;
};
img.onerror = () => {
hideProgress();
alertify.error('Failed to load upscaled image.');
this.isProcessing = false;
};
img.src = 'data:image/png;base64,' + result.result;
} catch (err) {
hideProgress();
alertify.error('Upscale failed: ' + (err.message || err));
this.isProcessing = false;
}
}
}
export default Image_upscale_class;