/** * 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 — ~1–3 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: `
Current: ${W}×${H}px
${deviceNote}
`, }, { 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( `
Installing Real-ESRGAN AI upscaler…
0%
`, 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 30–120 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;