PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/ai-tools-misc.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:01:21 UTC 10,099 bytes, 216 lines download raw bytes

1/**
2 * Misc AI-tool wiring — the three AI tools that don't share the
3 * inpaint pipeline:
4 *
5 *   Harmonize: Reinhard color transfer on a body mask (no AI redraw)
6 *              + an optional narrow inpaint on a seam mask if the
7 *              "Seam fix" slider > 0.
8 *   Canvas 2×/4× Upscale: in-browser bicubic resampling, no server.
9 *   AI Upscale: Real-ESRGAN via /api/image/upscale-local.
10 *   Style Transfer: img2img via /api/gallery/style-transfer.
11 *
12 * Plus the small `_addEmptyLayer` helper and its toolbar wiring,
13 * since it lived next to these.
14 *
15 * @param {{
16 *   apiBase:             string,
17 *   buildLayerBodyMask:  (featherPx: number) => string | null,
18 *   buildSeamMask:       (featherPx: number) => string | null,
19 *   applyImageTool:      (endpoint, payload, layerName, btn, opts?) => Promise<void>,
20 *   flatten:             () => HTMLCanvasElement,
21 *   saveState:           (label?: string) => void,
22 *   fitZoom:             () => void,
23 *   composite:           () => void,
24 *   createLayer:         (name, w, h) => object,
25 *   renderLayerPanel:    () => void,
26 *   spinnerModule:       object,
27 *   uiModule:            object,
28 * }} deps
29 *
30 * @returns {{ addEmptyLayer: () => void }}
31 */
32import { state } from './state.js';
33
34export function wireAIToolsMisc({
35  apiBase, buildLayerBodyMask, buildSeamMask, applyImageTool,
36  flatten, saveState, fitZoom, composite, createLayer, renderLayerPanel,
37  spinnerModule, uiModule,
38}) {
39  // ── Harmonize sliders — Color match + Seam fix ──
40  const harmColorPrev = document.getElementById('ge-harmonize-color-preview');
41  const harmSeamPrev = document.getElementById('ge-harmonize-seam-preview');
42  document.getElementById('ge-harmonize-color')?.addEventListener('input', (e) => {
43    document.getElementById('ge-harmonize-color-label').textContent = (e.target.value / 100).toFixed(2);
44    if (harmColorPrev) harmColorPrev.style.opacity = (parseInt(e.target.value, 10) / 100).toFixed(2);
45  });
46  document.getElementById('ge-harmonize-seam')?.addEventListener('input', (e) => {
47    document.getElementById('ge-harmonize-seam-label').textContent = (e.target.value / 100).toFixed(2);
48    if (harmSeamPrev) harmSeamPrev.style.opacity = (parseInt(e.target.value, 10) / 100).toFixed(2);
49  });
50
51  // Harmonize button — two-stage:
52  //   1) Reinhard color transfer on body mask (no AI redraw)
53  //   2) Optional narrow inpaint on seam mask (if seam_fix > 0)
54  document.getElementById('ge-harmonize-run')?.addEventListener('click', () => {
55    const prompt = document.getElementById('ge-harmonize-prompt')?.value?.trim() || 'photorealistic, natural lighting, seamless blend';
56    const color_match = (parseInt(document.getElementById('ge-harmonize-color')?.value || '65')) / 100;
57    const seam_fix = (parseInt(document.getElementById('ge-harmonize-seam')?.value || '0')) / 100;
58    const bodyFeather = Math.max(6, Math.round(Math.min(state.imgWidth, state.imgHeight) * 0.012));
59    const seamFeather = Math.max(8, Math.round(Math.min(state.imgWidth, state.imgHeight) * 0.015));
60    const body_mask = buildLayerBodyMask(bodyFeather);
61    const seam_mask = seam_fix > 0.01 ? buildSeamMask(seamFeather) : null;
62    // Harmonize needs a non-base layer to color-match against the
63    // background. Without one, the server would fall back to legacy
64    // whole-image img2img — i.e. regenerate the whole photo. Block
65    // that and tell the user what's missing.
66    if (!body_mask) {
67      if (uiModule) uiModule.showToast('Harmonize needs a second layer pasted/imported over the base photo — nothing to color-match against.', 6000);
68      return;
69    }
70    const payload = { prompt, color_match, seam_fix, body_mask };
71    if (seam_mask) payload.seam_mask = seam_mask;
72    applyImageTool('/api/image/harmonize', payload, 'Harmonized', document.getElementById('ge-harmonize-run'));
73  });
74
75  // ── Canvas upscale (bicubic) ──
76  function canvasUpscale(factor) {
77    saveState(`Upscale ${factor}×`);
78    const newW = state.imgWidth * factor;
79    const newH = state.imgHeight * factor;
80    state.layers.forEach(l => {
81      const tmp = document.createElement('canvas');
82      tmp.width = newW; tmp.height = newH;
83      const tCtx = tmp.getContext('2d');
84      tCtx.imageSmoothingEnabled = true;
85      tCtx.imageSmoothingQuality = 'high';
86      tCtx.drawImage(l.canvas, 0, 0, newW, newH);
87      l.canvas.width = newW; l.canvas.height = newH;
88      l.ctx.drawImage(tmp, 0, 0);
89    });
90    if (state.maskCanvas) { state.maskCanvas.width = newW; state.maskCanvas.height = newH; }
91    state.imgWidth = newW; state.imgHeight = newH;
92    state.mainCanvas.width = newW; state.mainCanvas.height = newH;
93    const sizeLabel = document.getElementById('ge-canvas-size');
94    if (sizeLabel) sizeLabel.textContent = `${newW}×${newH}`;
95    fitZoom();
96    composite();
97    uiModule.showToast(`Upscaled ${factor}× to ${newW}×${newH}`);
98  }
99  document.getElementById('ge-upscale-2x')?.addEventListener('click', () => canvasUpscale(2));
100  document.getElementById('ge-upscale-4x')?.addEventListener('click', () => canvasUpscale(4));
101
102  // ── AI upscale (Real-ESRGAN, no diffusion server required) ──
103  document.getElementById('ge-upscale-ai')?.addEventListener('click', async () => {
104    const btn = document.getElementById('ge-upscale-ai');
105    const origHTML = btn.innerHTML;
106    btn.disabled = true;
107    let upWp = null;
108    try {
109      upWp = spinnerModule.createWhirlpool(14);
110      upWp.element.style.cssText = 'display:inline-block;vertical-align:middle;position:relative;top:1px;margin-right:6px;width:14px;height:14px;';
111      btn.innerHTML = '';
112      btn.appendChild(upWp.element);
113      const lbl = document.createElement('span');
114      lbl.textContent = 'Upscaling…';
115      btn.appendChild(lbl);
116    } catch (_) { btn.textContent = 'Upscaling…'; }
117    try {
118      const flat = flatten();
119      const imageB64 = flat.toDataURL('image/png').split(',')[1];
120      const res = await fetch('/api/image/upscale-local', {
121        method: 'POST', credentials: 'same-origin',
122        headers: { 'Content-Type': 'application/json' },
123        body: JSON.stringify({ image: imageB64, scale: 2 }),
124      });
125      if (!res.ok) throw new Error('Server returned ' + res.status);
126      const data = await res.json();
127      if (data.image) {
128        const img = new Image();
129        img.onload = () => {
130          if (!state.editorOpen) return;
131          saveState();
132          const newW = img.width, newH = img.height;
133          const layer = createLayer('AI Upscaled', newW, newH);
134          layer.ctx.drawImage(img, 0, 0);
135          state.layers.push(layer);
136          state.activeLayerId = layer.id;
137          state.imgWidth = newW; state.imgHeight = newH;
138          state.mainCanvas.width = newW; state.mainCanvas.height = newH;
139          if (state.maskCanvas) { state.maskCanvas.width = newW; state.maskCanvas.height = newH; }
140          const sizeLabel = document.getElementById('ge-canvas-size');
141          if (sizeLabel) sizeLabel.textContent = `${newW}×${newH}`;
142          fitZoom();
143          composite();
144          renderLayerPanel();
145          uiModule.showToast(`AI upscaled to ${newW}×${newH}`);
146        };
147        img.src = 'data:image/png;base64,' + data.image;
148      } else {
149        throw new Error(data.error || 'No image returned');
150      }
151    } catch (e) {
152      uiModule.showToast('AI upscale failed: ' + e.message);
153    }
154    try { upWp?.destroy(); } catch (_) {}
155    btn.disabled = false;
156    btn.innerHTML = origHTML;
157  });
158
159  // ── Style transfer ──
160  document.getElementById('ge-style-strength')?.addEventListener('input', (e) => {
161    document.getElementById('ge-style-strength-label').textContent = (parseInt(e.target.value) / 100).toFixed(2);
162  });
163  document.getElementById('ge-style-run')?.addEventListener('click', async () => {
164    const btn = document.getElementById('ge-style-run');
165    const prompt = document.getElementById('ge-style-prompt').value.trim();
166    if (!prompt) { uiModule.showToast('Enter a style prompt'); return; }
167    const strength = parseInt(document.getElementById('ge-style-strength').value) / 100;
168    btn.disabled = true; btn.textContent = 'Applying...';
169    try {
170      const flat = flatten();
171      const blob = await new Promise(r => flat.toBlob(r, 'image/png'));
172      const fd = new FormData();
173      fd.append('image', blob, 'style.png');
174      fd.append('prompt', prompt);
175      fd.append('strength', String(strength));
176      const res = await fetch(`${apiBase}/api/gallery/style-transfer`, { method: 'POST', credentials: 'same-origin', body: fd });
177      if (!res.ok) throw new Error('Server returned ' + res.status);
178      const data = await res.json();
179      if (data.image) {
180        const img = new Image();
181        img.onload = () => {
182          if (!state.editorOpen) return;
183          saveState();
184          const layer = createLayer('Styled: ' + prompt.substring(0, 20), state.imgWidth, state.imgHeight);
185          layer.ctx.drawImage(img, 0, 0, state.imgWidth, state.imgHeight);
186          state.layers.push(layer);
187          state.activeLayerId = layer.id;
188          composite();
189          renderLayerPanel();
190          uiModule.showToast('Style applied');
191        };
192        img.src = 'data:image/png;base64,' + data.image;
193      } else {
194        throw new Error(data.error || 'No image returned');
195      }
196    } catch (e) {
197      uiModule.showToast('Style transfer failed: ' + e.message);
198    }
199    btn.disabled = false; btn.textContent = 'Apply Style';
200  });
201
202  // ── Add empty layer (used by the layer-panel header button + the
203  // Ctrl+Alt+J keyboard shortcut). Returned so keyboard-shortcuts.js
204  // can call it through the same path. ──
205  function addEmptyLayer() {
206    saveState('Add layer');
207    const layer = createLayer('Layer ' + state.layers.length, state.imgWidth, state.imgHeight);
208    state.layers.push(layer);
209    state.activeLayerId = layer.id;
210    renderLayerPanel();
211    composite();
212  }
213  document.getElementById('ge-add-layer')?.addEventListener('click', addEmptyLayer);
214
215  return { addEmptyLayer };
216}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.