Smart upscale: auto-detect hardware and pick best Real-ESRGAN path

Detection priority (probed once, cached):
  1. Real-ESRGAN PyTorch + CUDA GPU   → fastest, best quality
  2. Real-ESRGAN PyTorch + Apple MPS  → fast on Apple Silicon
  3. Real-ESRGAN NCNN Vulkan binary   → fast on any GPU via Vulkan (no CUDA needed)
  4. Real-ESRGAN PyTorch CPU          → works, slow (warned in UI)
  5. Lanczos                          → always available, instant fallback

Backend:
- services/upscale.py: full capability probe (probe_upscale_capabilities),
  implementations for PyTorch (CUDA/MPS/CPU auto-device) and NCNN binary,
  upscale_sync() resolves method with fallback chain,
  async upscale_image() runs in thread pool
- print_tools.py: /api/print/upscale uses new service; method="auto" by default;
  GET /api/print/upscale/available returns full capability map with device info
  and recommended_label; POST /api/print/upscale/refresh-caps busts cache
  without restart (useful after installing NCNN binary into container)

Frontend:
- upscale.js: fetches capability map on first open; builds method selector showing
  only available options; labels recommended method with ★; shows device info
  (CUDA/MPS/CPU/NCNN) in dialog; maps display label back to method key on submit;
  shows actual method used in success toast and undo history entry

Scripts:
- scripts/download_realesrgan.py: downloads NCNN Vulkan binary for current platform
  (Linux/macOS/Windows) to /app/data/models/realesrgan/; makes executable;
  run inside container or locally

https://claude.ai/code/session_01B58MaJCU1R6KwBDJCp8AfN
This commit is contained in:
Claude
2026-06-09 18:32:32 +00:00
parent 40396b72a0
commit cea9ee9d6c
4 changed files with 534 additions and 98 deletions
+93 -36
View File
@@ -1,9 +1,13 @@
/**
* Upscale — increase image resolution.
* Fetches available methods from /api/print/upscale/available on first open.
* Auto-selects the recommended method; user can override.
*
* Lanczos: always available, fast, good for clean/sharp images.
* AI (Real-ESRGAN): much better for photos — restores texture, sharpness.
* Requires `realesrgan-ncnn-vulkan` or `basicsr` + `realesrgan` Python packages.
* Methods (in priority order, server picks best):
* auto — server picks best available
* realesrgan_pytorch — Real-ESRGAN via PyTorch (CUDA > MPS > CPU)
* realesrgan_ncnn — Real-ESRGAN NCNN Vulkan binary (any GPU)
* lanczos — always available, instant
*
* Menu target: image/upscale.upscale
*/
@@ -16,6 +20,14 @@ import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.j
var instance = null;
// Method display labels
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() {
@@ -24,7 +36,7 @@ class Image_upscale_class {
this.Base_layers = new Base_layers_class();
this.Dialog = new Dialog_class();
this.isProcessing = false;
this._aiAvailable = null;
this._caps = null;
}
async upscale() {
@@ -33,24 +45,41 @@ class Image_upscale_class {
return;
}
var caps = await this._fetchCaps();
var W = config.layer.width_original;
var H = config.layer.height_original;
// Check AI availability once, cache it
if (this._aiAvailable === null) {
try {
var base = window.API_BASE_URL || '';
var r = await fetch(`${base}/api/print/upscale/available`);
var data = r.ok ? await r.json() : {};
this._aiAvailable = data.realesrgan || false;
} catch {
this._aiAvailable = false;
}
}
// Build method selector — only show what's available + auto
var available = ['auto', ...caps.methods];
var methodValues = [...new Set(available)]; // dedupe
var aiNote = this._aiAvailable
? 'Real-ESRGAN AI upscaling available.'
: 'AI upscaling not installed (Real-ESRGAN). Using Lanczos only.';
// Label each option, mark recommended
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;
});
// Annotate with device info
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) {
deviceNote = 'No AI upscaler detected — Lanczos only. ' +
'Install Real-ESRGAN for AI quality (see docs).';
}
var _this = this;
@@ -60,7 +89,8 @@ class Image_upscale_class {
{
title: '',
html: `<div style="font-size:11px;color:#888;margin:0 0 8px;">
Current size: ${W}×${H}px<br>${aiNote}
Current: ${W}×${H}px<br>
${deviceNote}
</div>`,
},
{
@@ -73,8 +103,8 @@ class Image_upscale_class {
{
name: 'method',
title: 'Method:',
value: this._aiAvailable ? 'ai' : 'lanczos',
values: this._aiAvailable ? ['lanczos', 'ai'] : ['lanczos'],
value: methodLabels[0], // auto
values: methodLabels,
type: 'select',
},
{
@@ -84,21 +114,49 @@ class Image_upscale_class {
},
],
on_finish: async function (params) {
// Map label back to method key
var labelIdx = methodLabels.indexOf(params.method);
var methodKey = labelIdx >= 0 ? methodValues[labelIdx] : 'auto';
var scale = parseFloat(params.scale);
var newW = Math.round(W * scale);
var newH = Math.round(H * scale);
await _this._run(scale, params.method, params.new_layer, newW, newH);
await _this._run(scale, methodKey, params.new_layer);
},
});
}
async _run(scale, method, newLayer, newW, newH) {
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 */ }
// Safe default if fetch failed
if (!this._caps) {
this._caps = {
lanczos: true,
realesrgan_pytorch: false,
realesrgan_ncnn: false,
recommended: 'lanczos',
recommended_label: 'Lanczos',
methods: ['lanczos'],
};
}
return this._caps;
}
async _run(scale, method, newLayer) {
if (this.isProcessing) return;
this.isProcessing = true;
alertify.message(
`Upscaling ${scale}× with ${method}... please wait`, 0
);
var caps = this._caps || {};
var methodLabel = method === 'auto'
? `Auto (${caps.recommended_label || 'best available'})`
: (METHOD_LABELS[method] || method);
alertify.message(`Upscaling ${scale}× · ${methodLabel}...`, 0);
try {
var layerCanvas = document.createElement('canvas');
@@ -111,11 +169,7 @@ class Image_upscale_class {
var r = await fetch(`${base}/api/print/upscale`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image: imageB64,
scale: scale,
method: method,
}),
body: JSON.stringify({ image: imageB64, scale, method }),
});
if (!r.ok) {
@@ -131,11 +185,15 @@ class Image_upscale_class {
resultCanvas.height = img.naturalHeight;
resultCanvas.getContext('2d').drawImage(img, 0, 0);
// Human-readable method label for undo history
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}× upscale (${result.method})`,
name: `${scale}× ${usedLabel}`,
type: 'image',
data: img.src,
x: 0, y: 0,
@@ -156,8 +214,7 @@ class Image_upscale_class {
alertify.dismissAll();
alertify.success(
`Upscaled to ${result.output.width}×${result.output.height}px` +
` (${result.method})`
`${result.output.width}×${result.output.height}px · ${usedLabel}`
);
this.isProcessing = false;
};