Spaces:
Sleeping
Sleeping
| /* ===== Character Reference (角色参考) Module ===== */ | |
| window.NAIReference = { | |
| image: null, // {dataUrl, b64Processed} — b64Processed is after resize_and_pad | |
| getInfoExtract() { | |
| const el = $('#reference-info-extract'); | |
| return el ? parseFloat(el.value) : 1; | |
| }, | |
| getStrength() { | |
| const el = $('#reference-strength'); | |
| return el ? parseFloat(el.value) : 1; | |
| }, | |
| getStyleMode() { | |
| const el = $('#reference-style-select'); | |
| return el ? el.value : 'character&style'; | |
| }, | |
| async handleUpload(file) { | |
| if (!file || !file.type.startsWith('image/')) return; | |
| const dataUrl = await NAIUtils.fileToDataUrl(file); | |
| const processed = await this.resizeAndPad(dataUrl); | |
| this.image = { dataUrl, b64Processed: processed }; | |
| this.renderPreview(); | |
| }, | |
| useLastGenerated() { | |
| const imgEl = document.querySelector('#result-image-area img'); | |
| if (!imgEl || !imgEl.src) return; | |
| const dataUrl = imgEl.src; | |
| // Process async | |
| this.resizeAndPad(dataUrl).then(processed => { | |
| this.image = { dataUrl, b64Processed: processed }; | |
| this.renderPreview(); | |
| }); | |
| }, | |
| clear() { | |
| this.image = null; | |
| this.renderPreview(); | |
| }, | |
| renderPreview() { | |
| const previewArea = $('#reference-preview'); | |
| const slidersArea = $('#reference-sliders'); | |
| const clearBtn = $('#btn-reference-clear'); | |
| if (!this.image) { | |
| previewArea.innerHTML = ''; | |
| previewArea.style.display = 'none'; | |
| slidersArea.innerHTML = ''; | |
| clearBtn.style.display = 'none'; | |
| return; | |
| } | |
| clearBtn.style.display = ''; | |
| previewArea.style.display = 'block'; | |
| previewArea.innerHTML = ` | |
| <div class="reference-preview-container"> | |
| <img src="${this.image.dataUrl}" class="reference-preview-img" /> | |
| </div>`; | |
| slidersArea.innerHTML = ''; | |
| slidersArea.appendChild(NAIUtils.createDynSlider('参考度', 'reference-info-extract', 0, 1, 0.05, 1)); | |
| slidersArea.appendChild(NAIUtils.createDynSlider('强度', 'reference-strength', 0, 1, 0.05, 1)); | |
| }, | |
| /** | |
| * Resize and pad image to nearest target size (1024x1536, 1472x1472, 1536x1024), | |
| * centered on black background. Returns base64 PNG string (no prefix). | |
| */ | |
| resizeAndPad(dataUrl) { | |
| return new Promise(async (resolve) => { | |
| const img = await NAIUtils.loadImageFromDataUrl(dataUrl); | |
| const ow = img.naturalWidth; | |
| const oh = img.naturalHeight; | |
| const ratio = ow / oh; | |
| // Find closest target size by aspect ratio | |
| const targets = [[1024, 1536], [1472, 1472], [1536, 1024]]; | |
| let best = targets[0]; | |
| let minDiff = Infinity; | |
| for (const [tw, th] of targets) { | |
| const diff = Math.abs(ratio - tw / th); | |
| if (diff < minDiff) { minDiff = diff; best = [tw, th]; } | |
| } | |
| const [tw, th] = best; | |
| // Scale to fit | |
| const scale = Math.min(tw / ow, th / oh); | |
| const nw = Math.round(ow * scale); | |
| const nh = Math.round(oh * scale); | |
| // Draw on canvas | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = tw; | |
| canvas.height = th; | |
| const ctx = canvas.getContext('2d'); | |
| // Black background | |
| ctx.fillStyle = '#000000'; | |
| ctx.fillRect(0, 0, tw, th); | |
| // Centered image | |
| const xOff = Math.floor((tw - nw) / 2); | |
| const yOff = Math.floor((th - nh) / 2); | |
| ctx.drawImage(img, xOff, yOff, nw, nh); | |
| // Export as PNG base64 | |
| const result = canvas.toDataURL('image/png'); | |
| resolve(NAIUtils.dataUrlToBase64(result)); | |
| }); | |
| }, | |
| collectParams() { | |
| if (!this.image) return null; | |
| return { | |
| image: this.image.b64Processed, | |
| info_extract: this.getInfoExtract(), | |
| strength: this.getStrength(), | |
| style_mode: this.getStyleMode(), | |
| }; | |
| }, | |
| init() { | |
| // Toggle | |
| $('#reference-toggle').addEventListener('click', () => { | |
| const c = $('#reference-content'); | |
| const a = $('#reference-arrow'); | |
| const open = c.classList.toggle('open'); | |
| a.textContent = open ? '▼' : '▶'; | |
| }); | |
| // Upload | |
| const input = $('#reference-input-file'); | |
| const drop = $('#reference-dropzone'); | |
| drop.addEventListener('click', () => input.click()); | |
| drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('dragover'); }); | |
| drop.addEventListener('dragleave', () => drop.classList.remove('dragover')); | |
| drop.addEventListener('drop', e => { | |
| e.preventDefault(); drop.classList.remove('dragover'); | |
| if (e.dataTransfer.files.length > 0) this.handleUpload(e.dataTransfer.files[0]); | |
| }); | |
| input.addEventListener('change', () => { | |
| if (input.files.length > 0) this.handleUpload(input.files[0]); | |
| input.value = ''; | |
| }); | |
| // Use last generated | |
| $('#btn-reference-use-last').addEventListener('click', () => this.useLastGenerated()); | |
| // Clear | |
| $('#btn-reference-clear').addEventListener('click', () => this.clear()); | |
| }, | |
| }; |