| |
| (function() { |
| const handle = document.getElementById('resize-handle'); |
| const viewer = document.getElementById('viewer-section'); |
| const library = document.getElementById('library-section'); |
| const workspace = document.querySelector('.workspace'); |
| let dragging = false, startY = 0, startVH = 0; |
|
|
| handle.addEventListener('mousedown', (e) => { |
| dragging = true; |
| startY = e.clientY; |
| startVH = viewer.getBoundingClientRect().height; |
| document.body.style.cursor = 'row-resize'; |
| document.body.style.userSelect = 'none'; |
| handle.querySelector('div').style.background = 'var(--accent)'; |
| e.preventDefault(); |
| }); |
| document.addEventListener('mousemove', (e) => { |
| if (!dragging) return; |
| const wsH = workspace.getBoundingClientRect().height; |
| const delta = e.clientY - startY; |
| let newVH = startVH + delta; |
| |
| newVH = Math.max(150, Math.min(wsH - 100 - 5, newVH)); |
| viewer.style.flex = 'none'; |
| viewer.style.height = newVH + 'px'; |
| library.style.flex = '1'; |
| }); |
| document.addEventListener('mouseup', () => { |
| if (dragging) { |
| dragging = false; |
| document.body.style.cursor = ''; |
| document.body.style.userSelect = ''; |
| handle.querySelector('div').style.background = 'var(--border)'; |
| } |
| }); |
| |
| handle.addEventListener('mouseenter', () => { handle.querySelector('div').style.background = 'var(--text-dim)'; }); |
| handle.addEventListener('mouseleave', () => { if (!dragging) handle.querySelector('div').style.background = 'var(--border)'; }); |
| })(); |
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
| const BASE = `http://${window.location.hostname}:3000`; |
|
|
| function _t(k) { |
| return typeof window.t === 'function' ? window.t(k) : k; |
| } |
| |
| let currentMode = 'image'; |
| let pollInterval = null; |
| let availableModels = []; |
| let availableLoras = []; |
|
|
| |
| console.log("Connecting to Backend API at:", BASE); |
|
|
| |
| async function scanModels() { |
| try { |
| const url = `${BASE}/api/models`; |
| console.log("Scanning models from:", url); |
| const res = await fetch(url); |
| const data = await res.json().catch(() => ({})); |
| console.log("Models response:", res.status, data); |
| if (!res.ok) { |
| const msg = data.message || data.error || res.statusText; |
| addLog(`โ ๆจกๅๆซๆๅคฑ่ดฅ (${res.status}): ${msg}`); |
| availableModels = []; |
| updateModelDropdown(); |
| updateBatchModelDropdown(); |
| return; |
| } |
| availableModels = data.models || []; |
| updateModelDropdown(); |
| updateBatchModelDropdown(); |
| if (availableModels.length > 0) { |
| addLog(`๐ ๅทฒๆซๆๅฐ ${availableModels.length} ไธชๆจกๅ: ${availableModels.map(m => m.name).join(', ')}`); |
| } |
| } catch (e) { |
| console.log("Model scan error:", e); |
| addLog(`โ ๆจกๅๆซๆๅผๅธธ: ${e.message || e}`); |
| } |
| } |
|
|
| function updateModelDropdown() { |
| const select = document.getElementById('vid-model'); |
| if (!select) return; |
| select.innerHTML = '<option value="">' + _t('defaultModel') + '</option>'; |
| availableModels.forEach(model => { |
| const opt = document.createElement('option'); |
| opt.value = model.path; |
| opt.textContent = model.name; |
| select.appendChild(opt); |
| }); |
| } |
|
|
| |
| async function scanLoras() { |
| try { |
| const url = `${BASE}/api/loras`; |
| console.log("Scanning LoRA from:", url); |
| const res = await fetch(url); |
| const data = await res.json().catch(() => ({})); |
| console.log("LoRA response:", res.status, data); |
| if (!res.ok) { |
| const msg = data.message || data.error || res.statusText; |
| addLog(`โ LoRA ๆซๆๅคฑ่ดฅ (${res.status}): ${msg}`); |
| availableLoras = []; |
| updateLoraDropdown(); |
| updateBatchLoraDropdown(); |
| return; |
| } |
| availableLoras = data.loras || []; |
| updateLoraDropdown(); |
| updateBatchLoraDropdown(); |
| if (data.loras_dir) { |
| const hintEl = document.getElementById('lora-placement-hint'); |
| if (hintEl) { |
| const tpl = _t('loraPlacementHintWithDir'); |
| hintEl.innerHTML = tpl.replace( |
| '{dir}', |
| escapeHtmlAttr(data.models_dir || data.loras_dir) |
| ); |
| } |
| } |
| if (availableLoras.length > 0) { |
| addLog(`๐ ๅทฒๆซๆๅฐ ${availableLoras.length} ไธช LoRA: ${availableLoras.map(l => l.name).join(', ')}`); |
| } |
| } catch (e) { |
| console.log("LoRA scan error:", e); |
| addLog(`โ LoRA ๆซๆๅผๅธธ: ${e.message || e}`); |
| } |
| } |
|
|
| function updateLoraDropdown() { |
| const select = document.getElementById('vid-lora'); |
| if (!select) return; |
| select.innerHTML = '<option value="">' + _t('noLora') + '</option>'; |
| availableLoras.forEach(lora => { |
| const opt = document.createElement('option'); |
| opt.value = lora.path; |
| opt.textContent = lora.name; |
| select.appendChild(opt); |
| }); |
| } |
|
|
| function updateLoraStrength() { |
| const select = document.getElementById('vid-lora'); |
| const container = document.getElementById('lora-strength-container'); |
| if (select && container) { |
| container.style.display = select.value ? 'flex' : 'none'; |
| } |
| } |
|
|
| |
| function updateBatchModelDropdown() { |
| const select = document.getElementById('batch-model'); |
| if (!select) return; |
| select.innerHTML = '<option value="">' + _t('defaultModel') + '</option>'; |
| availableModels.forEach(model => { |
| const opt = document.createElement('option'); |
| opt.value = model.path; |
| opt.textContent = model.name; |
| select.appendChild(opt); |
| }); |
| } |
|
|
| function updateBatchLoraDropdown() { |
| const select = document.getElementById('batch-lora'); |
| if (!select) return; |
| select.innerHTML = '<option value="">' + _t('noLora') + '</option>'; |
| availableLoras.forEach(lora => { |
| const opt = document.createElement('option'); |
| opt.value = lora.path; |
| opt.textContent = lora.name; |
| select.appendChild(opt); |
| }); |
| } |
|
|
| |
| function initBatchDropdowns() { |
| updateBatchModelDropdown(); |
| updateBatchLoraDropdown(); |
| } |
|
|
| |
|
|
| |
| (function() { |
| ['vid-quality', 'batch-quality'].forEach((id) => { |
| const sel = document.getElementById(id); |
| if (sel && sel.value === '544') sel.value = '540'; |
| }); |
| |
| setTimeout(() => { |
| scanModels(); |
| scanLoras(); |
| initBatchDropdowns(); |
| }, 1500); |
| })(); |
|
|
| |
| function updateResPreview() { |
| const q = document.getElementById('vid-quality').value; |
| const r = document.getElementById('vid-ratio').value; |
| |
| |
| let resLabel = q === "1080" ? "1080p" : q === "720" ? "720p" : "540p"; |
| |
| |
| let resDisplay; |
| if (r === "16:9") { |
| resDisplay = q === "1080" ? "1920x1088" : q === "720" ? "1280x704" : "1024x576"; |
| } else { |
| resDisplay = q === "1080" ? "1088x1920" : q === "720" ? "704x1280" : "576x1024"; |
| } |
| |
| document.getElementById('res-preview').innerText = `${_t('resPreviewPrefix')}: ${resLabel} (${resDisplay})`; |
| return resLabel; |
| } |
|
|
| |
| function updateImgResPreview() { |
| const w = document.getElementById('img-w').value; |
| const h = document.getElementById('img-h').value; |
| document.getElementById('img-res-preview').innerText = `${_t('resPreviewPrefix')}: ${w}x${h}`; |
| } |
|
|
| |
| function updateBatchResPreview() { |
| const q = document.getElementById('batch-quality').value; |
| const r = document.getElementById('batch-ratio').value; |
| let resLabel = q === "1080" ? "1080p" : q === "720" ? "720p" : "540p"; |
| let resDisplay; |
| if (r === "16:9") { |
| resDisplay = q === "1080" ? "1920x1088" : q === "720" ? "1280x704" : "1024x576"; |
| } else { |
| resDisplay = q === "1080" ? "1088x1920" : q === "720" ? "704x1280" : "576x1024"; |
| } |
| document.getElementById('batch-res-preview').innerText = `${_t('resPreviewPrefix')}: ${resLabel} (${resDisplay})`; |
| return resLabel; |
| } |
|
|
| |
| function updateBatchLoraStrength() { |
| const select = document.getElementById('batch-lora'); |
| const container = document.getElementById('batch-lora-strength-container'); |
| if (select && container) { |
| container.style.display = select.value ? 'flex' : 'none'; |
| } |
| } |
|
|
| |
| function applyImgPreset(val) { |
| if (val === "custom") { |
| document.getElementById('img-custom-res').style.display = 'flex'; |
| } else { |
| const [w, h] = val.split('x'); |
| document.getElementById('img-w').value = w; |
| document.getElementById('img-h').value = h; |
| updateImgResPreview(); |
| |
| |
| } |
| } |
|
|
|
|
|
|
| |
| async function handleFrameUpload(file, frameType) { |
| if (!file) return; |
|
|
| const preview = document.getElementById(`${frameType}-frame-preview`); |
| const placeholder = document.getElementById(`${frameType}-frame-placeholder`); |
| const clearOverlay = document.getElementById(`clear-${frameType}-frame-overlay`); |
|
|
| const previewReader = new FileReader(); |
| previewReader.onload = (e) => { |
| preview.src = e.target.result; |
| preview.style.display = 'block'; |
| placeholder.style.display = 'none'; |
| clearOverlay.style.display = 'flex'; |
| }; |
| previewReader.readAsDataURL(file); |
|
|
| const reader = new FileReader(); |
| reader.onload = async (e) => { |
| const b64Data = e.target.result; |
| addLog(`ๆญฃๅจไธไผ ${frameType === 'start' ? '่ตทๅงๅธง' : '็ปๆๅธง'}: ${file.name}...`); |
| try { |
| const res = await fetch(`${BASE}/api/system/upload-image`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ image: b64Data, filename: file.name }) |
| }); |
| const data = await res.json(); |
| if (res.ok && data.path) { |
| document.getElementById(`${frameType}-frame-path`).value = data.path; |
| addLog(`โ
${frameType === 'start' ? '่ตทๅงๅธง' : '็ปๆๅธง'}ไธไผ ๆๅ`); |
| } else { |
| throw new Error(data.error || data.detail || "ไธไผ ๅคฑ่ดฅ"); |
| } |
| } catch (e) { |
| addLog(`โ ๅธงๅพ็ไธไผ ๅคฑ่ดฅ: ${e.message}`); |
| } |
| }; |
| reader.readAsDataURL(file); |
| } |
|
|
| function clearFrame(frameType) { |
| document.getElementById(`${frameType}-frame-input`).value = ""; |
| document.getElementById(`${frameType}-frame-path`).value = ""; |
| document.getElementById(`${frameType}-frame-preview`).style.display = 'none'; |
| document.getElementById(`${frameType}-frame-preview`).src = ""; |
| document.getElementById(`${frameType}-frame-placeholder`).style.display = 'block'; |
| document.getElementById(`clear-${frameType}-frame-overlay`).style.display = 'none'; |
| addLog(`๐งน ๅทฒๆธ
้ค${frameType === 'start' ? '่ตทๅงๅธง' : '็ปๆๅธง'}`); |
| } |
|
|
| |
| async function handleImageUpload(file) { |
| if (!file) return; |
| |
| |
| const preview = document.getElementById('upload-preview'); |
| const placeholder = document.getElementById('upload-placeholder'); |
| const clearOverlay = document.getElementById('clear-img-overlay'); |
| |
| const previewReader = new FileReader(); |
| preview.onload = () => { |
| preview.style.display = 'block'; |
| placeholder.style.display = 'none'; |
| clearOverlay.style.display = 'flex'; |
| }; |
| previewReader.onload = (e) => preview.src = e.target.result; |
| previewReader.readAsDataURL(file); |
|
|
| |
| const reader = new FileReader(); |
| reader.onload = async (e) => { |
| const b64Data = e.target.result; |
| addLog(`ๆญฃๅจไธไผ ๅ่ๅพ: ${file.name}...`); |
| try { |
| const res = await fetch(`${BASE}/api/system/upload-image`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| image: b64Data, |
| filename: file.name |
| }) |
| }); |
| const data = await res.json(); |
| if (res.ok && data.path) { |
| document.getElementById('uploaded-img-path').value = data.path; |
| addLog(`โ
ๅ่ๅพไธไผ ๆๅ: ${file.name}`); |
| } else { |
| const errMsg = data.error || data.detail || "ไธไผ ๅคฑ่ดฅ"; |
| throw new Error(typeof errMsg === 'string' ? errMsg : JSON.stringify(errMsg)); |
| } |
| } catch (e) { |
| addLog(`โ ๅพ็ไธไผ ๅคฑ่ดฅ: ${e.message}`); |
| } |
| }; |
| reader.onerror = () => addLog("โ ่ฏปๅๆฌๅฐๆไปถๅคฑ่ดฅ"); |
| reader.readAsDataURL(file); |
| } |
|
|
| function clearUploadedImage() { |
| document.getElementById('vid-image-input').value = ""; |
| document.getElementById('uploaded-img-path').value = ""; |
| document.getElementById('upload-preview').style.display = 'none'; |
| document.getElementById('upload-preview').src = ""; |
| document.getElementById('upload-placeholder').style.display = 'block'; |
| document.getElementById('clear-img-overlay').style.display = 'none'; |
| addLog("๐งน ๅทฒๆธ
้คๅ่ๅพ"); |
| } |
|
|
| |
| async function handleAudioUpload(file) { |
| if (!file) return; |
|
|
| const placeholder = document.getElementById('audio-upload-placeholder'); |
| const statusDiv = document.getElementById('audio-upload-status'); |
| const filenameStatus = document.getElementById('audio-filename-status'); |
| const clearOverlay = document.getElementById('clear-audio-overlay'); |
|
|
| placeholder.style.display = 'none'; |
| filenameStatus.innerText = file.name; |
| statusDiv.style.display = 'block'; |
| clearOverlay.style.display = 'flex'; |
|
|
| const reader = new FileReader(); |
| reader.onload = async (e) => { |
| const b64Data = e.target.result; |
| addLog(`ๆญฃๅจไธไผ ้ณ้ข: ${file.name}...`); |
| try { |
| |
| const res = await fetch(`${BASE}/api/system/upload-image`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| image: b64Data, |
| filename: file.name |
| }) |
| }); |
| const data = await res.json(); |
| if (res.ok && data.path) { |
| document.getElementById('uploaded-audio-path').value = data.path; |
| addLog(`โ
้ณ้ขไธไผ ๆๅ: ${file.name}`); |
| } else { |
| const errMsg = data.error || data.detail || "ไธไผ ๅคฑ่ดฅ"; |
| throw new Error(typeof errMsg === 'string' ? errMsg : JSON.stringify(errMsg)); |
| } |
| } catch (e) { |
| addLog(`โ ้ณ้ขไธไผ ๅคฑ่ดฅ: ${e.message}`); |
| } |
| }; |
| reader.onerror = () => addLog("โ ่ฏปๅๆฌๅฐ้ณ้ขๆไปถๅคฑ่ดฅ"); |
| reader.readAsDataURL(file); |
| } |
|
|
| function clearUploadedAudio() { |
| document.getElementById('vid-audio-input').value = ""; |
| document.getElementById('uploaded-audio-path').value = ""; |
| document.getElementById('audio-upload-placeholder').style.display = 'block'; |
| document.getElementById('audio-upload-status').style.display = 'none'; |
| document.getElementById('clear-audio-overlay').style.display = 'none'; |
| addLog("๐งน ๅทฒๆธ
้ค้ณ้ขๆไปถ"); |
| } |
|
|
| |
| async function handleUpscaleVideoUpload(file) { |
| if (!file) return; |
| const placeholder = document.getElementById('upscale-placeholder'); |
| const statusDiv = document.getElementById('upscale-status'); |
| const filenameStatus = document.getElementById('upscale-filename'); |
| const clearOverlay = document.getElementById('clear-upscale-overlay'); |
|
|
| filenameStatus.innerText = file.name; |
| placeholder.style.display = 'none'; |
| statusDiv.style.display = 'block'; |
| clearOverlay.style.display = 'flex'; |
|
|
| const reader = new FileReader(); |
| reader.onload = async (e) => { |
| const b64Data = e.target.result; |
| addLog(`ๆญฃๅจไธไผ ๅพ
่ถ
ๅ่ง้ข: ${file.name}...`); |
| try { |
| const res = await fetch(`${BASE}/api/system/upload-image`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ image: b64Data, filename: file.name }) |
| }); |
| const data = await res.json(); |
| if (res.ok && data.path) { |
| document.getElementById('upscale-video-path').value = data.path; |
| addLog(`โ
่ง้ขไธไผ ๆๅ`); |
| } else { |
| throw new Error(data.error || "ไธไผ ๅคฑ่ดฅ"); |
| } |
| } catch (e) { |
| addLog(`โ ่ง้ขไธไผ ๅคฑ่ดฅ: ${e.message}`); |
| } |
| }; |
| reader.readAsDataURL(file); |
| } |
|
|
| function clearUpscaleVideo() { |
| document.getElementById('upscale-video-input').value = ""; |
| document.getElementById('upscale-video-path').value = ""; |
| document.getElementById('upscale-placeholder').style.display = 'block'; |
| document.getElementById('upscale-status').style.display = 'none'; |
| document.getElementById('clear-upscale-overlay').style.display = 'none'; |
| addLog("๐งน ๅทฒๆธ
้คๅพ
่ถ
ๅ่ง้ข"); |
| } |
|
|
| |
| function initDragAndDrop() { |
| const audioDropZone = document.getElementById('audio-drop-zone'); |
| const startFrameDropZone = document.getElementById('start-frame-drop-zone'); |
| const endFrameDropZone = document.getElementById('end-frame-drop-zone'); |
| const upscaleDropZone = document.getElementById('upscale-drop-zone'); |
| const batchImagesDropZone = document.getElementById('batch-images-drop-zone'); |
| |
| const zones = [audioDropZone, startFrameDropZone, endFrameDropZone, upscaleDropZone, batchImagesDropZone]; |
|
|
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| zones.forEach(zone => { |
| if (!zone) return; |
| zone.addEventListener(eventName, (e) => { |
| e.preventDefault(); |
| e.stopPropagation(); |
| }, false); |
| }); |
| }); |
|
|
| ['dragenter', 'dragover'].forEach(eventName => { |
| zones.forEach(zone => { |
| if (!zone) return; |
| zone.addEventListener(eventName, () => zone.classList.add('dragover'), false); |
| }); |
| }); |
|
|
| ['dragleave', 'drop'].forEach(eventName => { |
| zones.forEach(zone => { |
| if (!zone) return; |
| zone.addEventListener(eventName, () => zone.classList.remove('dragover'), false); |
| }); |
| }); |
|
|
| audioDropZone.addEventListener('drop', (e) => { |
| const file = e.dataTransfer.files[0]; |
| if (file && file.type.startsWith('audio/')) handleAudioUpload(file); |
| }, false); |
|
|
| startFrameDropZone.addEventListener('drop', (e) => { |
| const file = e.dataTransfer.files[0]; |
| if (file && file.type.startsWith('image/')) handleFrameUpload(file, 'start'); |
| }, false); |
|
|
| endFrameDropZone.addEventListener('drop', (e) => { |
| const file = e.dataTransfer.files[0]; |
| if (file && file.type.startsWith('image/')) handleFrameUpload(file, 'end'); |
| }, false); |
|
|
| upscaleDropZone.addEventListener('drop', (e) => { |
| const file = e.dataTransfer.files[0]; |
| if (file && file.type.startsWith('video/')) handleUpscaleVideoUpload(file); |
| }, false); |
|
|
| |
| if (batchImagesDropZone) { |
| batchImagesDropZone.addEventListener('drop', (e) => { |
| e.preventDefault(); |
| e.stopPropagation(); |
| batchImagesDropZone.classList.remove('dragover'); |
| const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/')); |
| if (files.length > 0) handleBatchImagesUpload(files); |
| }, false); |
| } |
| } |
|
|
| |
| let batchImages = []; |
| |
| const batchKfStrengthByPath = {}; |
| const batchKfSegDurByIndex = {}; |
|
|
| function escapeHtmlAttr(s) { |
| return String(s) |
| .replace(/&/g, '&') |
| .replace(/"/g, '"') |
| .replace(/</g, '<'); |
| } |
|
|
| function defaultKeyframeStrengthForIndex(i, n) { |
| if (n <= 2) return '1'; |
| if (i === 0) return '0.62'; |
| if (i === n - 1) return '1'; |
| return '0.42'; |
| } |
|
|
| function captureBatchKfTimelineFromDom() { |
| batchImages.forEach((img, i) => { |
| if (!img.path) return; |
| const sEl = document.getElementById(`batch-kf-strength-${i}`); |
| if (sEl) batchKfStrengthByPath[img.path] = sEl.value.trim(); |
| }); |
| const n = batchImages.length; |
| for (let j = 0; j < n - 1; j++) { |
| const el = document.getElementById(`batch-kf-seg-dur-${j}`); |
| if (el) batchKfSegDurByIndex[j] = el.value.trim(); |
| } |
| } |
|
|
| |
| function readBatchKfSegmentSeconds(n, minSeg) { |
| const seg = []; |
| for (let j = 0; j < n - 1; j++) { |
| let v = parseFloat(document.getElementById(`batch-kf-seg-dur-${j}`)?.value); |
| if (!Number.isFinite(v) || v < minSeg) v = minSeg; |
| seg.push(v); |
| } |
| return seg; |
| } |
|
|
| function updateBatchKfTimelineDerivedUI() { |
| if (!batchWorkflowIsSingle() || batchImages.length < 2) return; |
| const n = batchImages.length; |
| const minSeg = 0.1; |
| const seg = readBatchKfSegmentSeconds(n, minSeg); |
| let t = 0; |
| for (let i = 0; i < n; i++) { |
| const label = document.getElementById(`batch-kf-anchor-label-${i}`); |
| if (!label) continue; |
| if (i === 0) { |
| label.textContent = `0.0 s ยท ${_t('batchAnchorStart')}`; |
| } else { |
| t += seg[i - 1]; |
| label.textContent = |
| i === n - 1 |
| ? `${t.toFixed(1)} s ยท ${_t('batchAnchorEnd')}` |
| : `${t.toFixed(1)} s`; |
| } |
| } |
| const totalEl = document.getElementById('batch-kf-total-seconds'); |
| if (totalEl) { |
| const sum = seg.reduce((a, b) => a + b, 0); |
| totalEl.textContent = sum.toFixed(1); |
| } |
| } |
| async function handleBatchImagesUpload(files, append = true) { |
| if (!files || files.length === 0) return; |
| addLog(`ๆญฃๅจไธไผ ${files.length} ๅผ ๅพ็...`); |
|
|
| for (let i = 0; i < files.length; i++) { |
| const file = files[i]; |
| const reader = new FileReader(); |
|
|
| const imgData = await new Promise((resolve) => { |
| reader.onload = async (e) => { |
| const b64Data = e.target.result; |
| try { |
| const res = await fetch(`${BASE}/api/system/upload-image`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ image: b64Data, filename: file.name }) |
| }); |
| const data = await res.json(); |
| if (res.ok && data.path) { |
| resolve({ name: file.name, path: data.path, preview: e.target.result }); |
| } else { |
| resolve(null); |
| } |
| } catch (e) { |
| resolve(null); |
| } |
| }; |
| reader.readAsDataURL(file); |
| }); |
|
|
| if (imgData) { |
| batchImages.push(imgData); |
| addLog(`โ
ๅพ็ ${i + 1}/${files.length} ไธไผ ๆๅ: ${file.name}`); |
| } |
| } |
|
|
| renderBatchImages(); |
| updateBatchSegments(); |
| } |
|
|
| async function handleBatchBackgroundAudioUpload(file) { |
| if (!file) return; |
| const ph = document.getElementById('batch-audio-placeholder'); |
| const st = document.getElementById('batch-audio-status'); |
| const overlay = document.getElementById('clear-batch-audio-overlay'); |
| const reader = new FileReader(); |
| reader.onload = async (e) => { |
| const b64Data = e.target.result; |
| addLog(`ๆญฃๅจไธไผ ๆ็้
ไน: ${file.name}...`); |
| try { |
| const res = await fetch(`${BASE}/api/system/upload-image`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ image: b64Data, filename: file.name }) |
| }); |
| const data = await res.json(); |
| if (res.ok && data.path) { |
| const hid = document.getElementById('batch-background-audio-path'); |
| if (hid) hid.value = data.path; |
| if (ph) ph.style.display = 'none'; |
| if (st) { |
| st.style.display = 'block'; |
| st.textContent = 'โ ' + file.name; |
| } |
| if (overlay) overlay.style.display = 'flex'; |
| addLog('โ
ๆ็้
ไนๅทฒไธไผ ๏ผๅฐ่ฆ็ๅ็ๆฎต่ชๅธฆ้ณ่ฝจ๏ผ'); |
| } else { |
| addLog(`โ ้
ไนไธไผ ๅคฑ่ดฅ: ${data.error || 'ๆช็ฅ้่ฏฏ'}`); |
| } |
| } catch (err) { |
| addLog(`โ ้
ไนไธไผ ๅคฑ่ดฅ: ${err.message}`); |
| } |
| }; |
| reader.onerror = () => addLog('โ ่ฏปๅ้ณ้ขๆไปถๅคฑ่ดฅ'); |
| reader.readAsDataURL(file); |
| } |
|
|
| function clearBatchBackgroundAudio() { |
| const hid = document.getElementById('batch-background-audio-path'); |
| const inp = document.getElementById('batch-audio-input'); |
| if (hid) hid.value = ''; |
| if (inp) inp.value = ''; |
| const ph = document.getElementById('batch-audio-placeholder'); |
| const st = document.getElementById('batch-audio-status'); |
| const overlay = document.getElementById('clear-batch-audio-overlay'); |
| if (ph) ph.style.display = 'block'; |
| if (st) { |
| st.style.display = 'none'; |
| st.textContent = ''; |
| } |
| if (overlay) overlay.style.display = 'none'; |
| addLog('๐งน ๅทฒๆธ
้คๆ็้
ไน'); |
| } |
|
|
| function syncBatchDropZoneChrome() { |
| const dropZone = document.getElementById('batch-images-drop-zone'); |
| const placeholder = document.getElementById('batch-images-placeholder'); |
| const stripWrap = document.getElementById('batch-thumb-strip-wrap'); |
| if (batchImages.length === 0) { |
| if (dropZone) { |
| dropZone.classList.remove('has-images'); |
| const mini = dropZone.querySelector('.upload-placeholder-mini'); |
| if (mini) mini.remove(); |
| } |
| if (placeholder) placeholder.style.display = 'block'; |
| if (stripWrap) stripWrap.style.display = 'none'; |
| return; |
| } |
| if (placeholder) placeholder.style.display = 'none'; |
| if (dropZone) dropZone.classList.add('has-images'); |
| if (stripWrap) stripWrap.style.display = 'block'; |
| if (dropZone && !dropZone.querySelector('.upload-placeholder-mini')) { |
| const mini = document.createElement('div'); |
| mini.className = 'upload-placeholder-mini'; |
| mini.innerHTML = '<span>' + _t('batchAddMore') + '</span>'; |
| dropZone.appendChild(mini); |
| } |
| } |
|
|
| let batchDragPlaceholderEl = null; |
| let batchPointerState = null; |
| let batchPendingPhX = null; |
| let batchPhMoveRaf = null; |
|
|
| function batchRemoveFloatingGhost() { |
| document.querySelectorAll('.batch-thumb-floating-ghost').forEach((n) => n.remove()); |
| } |
|
|
| function batchCancelPhMoveRaf() { |
| if (batchPhMoveRaf != null) { |
| cancelAnimationFrame(batchPhMoveRaf); |
| batchPhMoveRaf = null; |
| } |
| batchPendingPhX = null; |
| } |
|
|
| function batchEnsurePlaceholder() { |
| if (batchDragPlaceholderEl && batchDragPlaceholderEl.isConnected) return batchDragPlaceholderEl; |
| const el = document.createElement('div'); |
| el.className = 'batch-thumb-drop-slot'; |
| el.setAttribute('aria-hidden', 'true'); |
| batchDragPlaceholderEl = el; |
| return el; |
| } |
|
|
| function batchRemovePlaceholder() { |
| if (batchDragPlaceholderEl && batchDragPlaceholderEl.parentNode) { |
| batchDragPlaceholderEl.parentNode.removeChild(batchDragPlaceholderEl); |
| } |
| } |
|
|
| function batchComputeInsertIndex(container, placeholder) { |
| let t = 0; |
| for (const child of container.children) { |
| if (child === placeholder) return t; |
| if (child.classList && child.classList.contains('batch-image-wrapper')) { |
| if (!child.classList.contains('batch-thumb--source')) t++; |
| } |
| } |
| return t; |
| } |
|
|
| function batchMovePlaceholderFromPoint(container, clientX) { |
| const ph = batchEnsurePlaceholder(); |
| const wrappers = [...container.querySelectorAll('.batch-image-wrapper')]; |
| let insertBefore = null; |
| for (const w of wrappers) { |
| if (w.classList.contains('batch-thumb--source')) continue; |
| const r = w.getBoundingClientRect(); |
| if (clientX < r.left + r.width / 2) { |
| insertBefore = w; |
| break; |
| } |
| } |
| if (insertBefore === null) { |
| const vis = wrappers.filter((w) => !w.classList.contains('batch-thumb--source')); |
| const last = vis[vis.length - 1]; |
| if (last) { |
| if (last.nextSibling) { |
| container.insertBefore(ph, last.nextSibling); |
| } else { |
| container.appendChild(ph); |
| } |
| } else { |
| container.appendChild(ph); |
| } |
| } else { |
| container.insertBefore(ph, insertBefore); |
| } |
| } |
|
|
| function batchFlushPlaceholderMove() { |
| batchPhMoveRaf = null; |
| if (!batchPointerState || batchPendingPhX == null) return; |
| batchMovePlaceholderFromPoint(batchPointerState.container, batchPendingPhX); |
| } |
|
|
| function handleBatchPointerMove(e) { |
| if (!batchPointerState) return; |
| e.preventDefault(); |
| const st = batchPointerState; |
| st.ghostTX = e.clientX - st.offsetX; |
| st.ghostTY = e.clientY - st.offsetY; |
| batchPendingPhX = e.clientX; |
| if (batchPhMoveRaf == null) { |
| batchPhMoveRaf = requestAnimationFrame(batchFlushPlaceholderMove); |
| } |
| } |
|
|
| function batchGhostFrame() { |
| const st = batchPointerState; |
| if (!st || !st.ghostEl || !st.ghostEl.isConnected) { |
| return; |
| } |
| const t = 0.42; |
| st.ghostCX += (st.ghostTX - st.ghostCX) * t; |
| st.ghostCY += (st.ghostTY - st.ghostCY) * t; |
| st.ghostEl.style.transform = |
| `translate3d(${st.ghostCX}px,${st.ghostCY}px,0) scale(1.06) rotate(-1deg)`; |
| st.ghostRaf = requestAnimationFrame(batchGhostFrame); |
| } |
|
|
| function batchStartGhostLoop() { |
| const st = batchPointerState; |
| if (!st || !st.ghostEl) return; |
| if (st.ghostRaf != null) cancelAnimationFrame(st.ghostRaf); |
| st.ghostRaf = requestAnimationFrame(batchGhostFrame); |
| } |
|
|
| function batchEndPointerDrag(e) { |
| if (!batchPointerState) return; |
| if (e.pointerId !== batchPointerState.pointerId) return; |
| const st = batchPointerState; |
|
|
| batchCancelPhMoveRaf(); |
| if (st.ghostRaf != null) { |
| cancelAnimationFrame(st.ghostRaf); |
| st.ghostRaf = null; |
| } |
| if (st.ghostEl && st.ghostEl.parentNode) { |
| st.ghostEl.remove(); |
| } |
| batchPointerState = null; |
|
|
| document.removeEventListener('pointermove', handleBatchPointerMove); |
| document.removeEventListener('pointerup', batchEndPointerDrag); |
| document.removeEventListener('pointercancel', batchEndPointerDrag); |
|
|
| try { |
| if (st.wrapperEl) st.wrapperEl.releasePointerCapture(st.pointerId); |
| } catch (_) {} |
|
|
| const { fromIndex, container, wrapperEl } = st; |
| container.classList.remove('is-batch-settling'); |
| if (!batchDragPlaceholderEl || !batchDragPlaceholderEl.parentNode) { |
| if (wrapperEl) wrapperEl.classList.remove('batch-thumb--source'); |
| renderBatchImages(); |
| updateBatchSegments(); |
| return; |
| } |
| const to = batchComputeInsertIndex(container, batchDragPlaceholderEl); |
| batchRemovePlaceholder(); |
| if (wrapperEl) wrapperEl.classList.remove('batch-thumb--source'); |
|
|
| if (fromIndex !== to && fromIndex >= 0 && to >= 0) { |
| const [item] = batchImages.splice(fromIndex, 1); |
| batchImages.splice(to, 0, item); |
| updateBatchSegments(); |
| } |
| renderBatchImages(); |
| } |
|
|
| function handleBatchPointerDown(e) { |
| if (batchPointerState) return; |
| if (e.button !== 0) return; |
| if (e.target.closest && e.target.closest('.batch-thumb-remove')) return; |
|
|
| const wrapper = e.currentTarget; |
| const container = document.getElementById('batch-images-container'); |
| if (!container) return; |
|
|
| e.preventDefault(); |
| e.stopPropagation(); |
|
|
| const fromIndex = parseInt(wrapper.dataset.index, 10); |
| if (Number.isNaN(fromIndex)) return; |
|
|
| const rect = wrapper.getBoundingClientRect(); |
| const offsetX = e.clientX - rect.left; |
| const offsetY = e.clientY - rect.top; |
| const startLeft = rect.left; |
| const startTop = rect.top; |
|
|
| const ghost = document.createElement('div'); |
| ghost.className = 'batch-thumb-floating-ghost'; |
| const gImg = document.createElement('img'); |
| const srcImg = wrapper.querySelector('img'); |
| gImg.src = srcImg ? srcImg.src : ''; |
| gImg.alt = ''; |
| ghost.appendChild(gImg); |
| document.body.appendChild(ghost); |
|
|
| batchPointerState = { |
| fromIndex, |
| pointerId: e.pointerId, |
| wrapperEl: wrapper, |
| container, |
| ghostEl: ghost, |
| offsetX, |
| offsetY, |
| ghostTX: e.clientX - offsetX, |
| ghostTY: e.clientY - offsetY, |
| ghostCX: startLeft, |
| ghostCY: startTop, |
| ghostRaf: null |
| }; |
|
|
| ghost.style.transform = |
| `translate3d(${startLeft}px,${startTop}px,0) scale(1.06) rotate(-1deg)`; |
|
|
| container.classList.add('is-batch-settling'); |
| wrapper.classList.add('batch-thumb--source'); |
| const ph = batchEnsurePlaceholder(); |
| container.insertBefore(ph, wrapper.nextSibling); |
| |
| requestAnimationFrame(() => { |
| requestAnimationFrame(() => { |
| container.classList.remove('is-batch-settling'); |
| }); |
| }); |
|
|
| batchStartGhostLoop(); |
|
|
| document.addEventListener('pointermove', handleBatchPointerMove, { passive: false }); |
| document.addEventListener('pointerup', batchEndPointerDrag); |
| document.addEventListener('pointercancel', batchEndPointerDrag); |
|
|
| try { |
| wrapper.setPointerCapture(e.pointerId); |
| } catch (_) {} |
| } |
|
|
| function removeBatchImage(index) { |
| if (index < 0 || index >= batchImages.length) return; |
| batchImages.splice(index, 1); |
| renderBatchImages(); |
| updateBatchSegments(); |
| } |
|
|
| |
| function renderBatchImages() { |
| const container = document.getElementById('batch-images-container'); |
| if (!container) return; |
|
|
| syncBatchDropZoneChrome(); |
| batchRemovePlaceholder(); |
| batchCancelPhMoveRaf(); |
| batchRemoveFloatingGhost(); |
| batchPointerState = null; |
| container.classList.remove('is-batch-settling'); |
| container.innerHTML = ''; |
|
|
| batchImages.forEach((img, index) => { |
| const wrapper = document.createElement('div'); |
| wrapper.className = 'batch-image-wrapper'; |
| wrapper.dataset.index = String(index); |
| wrapper.title = _t('batchThumbDrag'); |
|
|
| const imgWrap = document.createElement('div'); |
| imgWrap.className = 'batch-thumb-img-wrap'; |
| const im = document.createElement('img'); |
| im.className = 'batch-thumb-img'; |
| im.src = img.preview; |
| im.alt = img.name || ''; |
| im.draggable = false; |
| imgWrap.appendChild(im); |
|
|
| const del = document.createElement('button'); |
| del.type = 'button'; |
| del.className = 'batch-thumb-remove'; |
| del.title = _t('batchThumbRemove'); |
| del.setAttribute('aria-label', _t('batchThumbRemove')); |
| del.textContent = 'ร'; |
| del.addEventListener('pointerdown', (ev) => ev.stopPropagation()); |
| del.addEventListener('click', (ev) => { |
| ev.stopPropagation(); |
| removeBatchImage(index); |
| }); |
|
|
| wrapper.appendChild(imgWrap); |
| wrapper.appendChild(del); |
|
|
| wrapper.addEventListener('pointerdown', handleBatchPointerDown); |
|
|
| container.appendChild(wrapper); |
| }); |
| } |
|
|
| function batchWorkflowIsSingle() { |
| const r = document.querySelector('input[name="batch-workflow"]:checked'); |
| return !!(r && r.value === 'single'); |
| } |
|
|
| function onBatchWorkflowChange() { |
| updateBatchSegments(); |
| } |
|
|
| |
| function updateBatchSegments() { |
| const container = document.getElementById('batch-segments-container'); |
| if (!container) return; |
| |
| if (batchImages.length < 2) { |
| container.innerHTML = |
| '<div style="color: var(--text-dim); font-size: 11px;">' + |
| escapeHtmlAttr(_t('batchNeedTwo')) + |
| '</div>'; |
| return; |
| } |
|
|
| if (batchWorkflowIsSingle()) { |
| if (batchImages.length >= 2) captureBatchKfTimelineFromDom(); |
| const n = batchImages.length; |
| const defaultTotal = 8; |
| const defaultSeg = |
| n > 1 ? (defaultTotal / (n - 1)).toFixed(1) : '4'; |
| let blocks = ''; |
| batchImages.forEach((img, i) => { |
| const path = img.path || ''; |
| const stDef = defaultKeyframeStrengthForIndex(i, n); |
| const stStored = batchKfStrengthByPath[path]; |
| const stVal = stStored !== undefined && stStored !== '' |
| ? escapeHtmlAttr(stStored) |
| : stDef; |
| const prev = escapeHtmlAttr(img.preview || ''); |
| if (i > 0) { |
| const j = i - 1; |
| const sdStored = batchKfSegDurByIndex[j]; |
| const segVal = |
| sdStored !== undefined && sdStored !== '' |
| ? escapeHtmlAttr(sdStored) |
| : defaultSeg; |
| blocks += ` |
| <div class="batch-kf-gap"> |
| <div class="batch-kf-gap-rail" aria-hidden="true"></div> |
| <div class="batch-kf-gap-inner"> |
| <span class="batch-kf-gap-ix">${i}โ${i + 1}</span> |
| <label class="batch-kf-seg-field"> |
| <input type="number" class="batch-kf-seg-input" id="batch-kf-seg-dur-${j}" |
| value="${segVal}" min="0.1" max="120" step="0.1" |
| title="${escapeHtmlAttr(_t('batchGapInputTitle'))}" |
| oninput="updateBatchKfTimelineDerivedUI()"> |
| <span class="batch-kf-gap-unit">${escapeHtmlAttr(_t('batchSec'))}</span> |
| </label> |
| </div> |
| </div>`; |
| } |
| blocks += ` |
| <div class="batch-kf-kcard"> |
| <div class="batch-kf-kcard-head"> |
| <img class="batch-kf-kthumb" src="${prev}" alt=""> |
| <div class="batch-kf-kcard-titles"> |
| <span class="batch-kf-ktitle">${escapeHtmlAttr(_t('batchKfTitle'))} ${i + 1} / ${n}</span> |
| <span class="batch-kf-anchor" id="batch-kf-anchor-label-${i}">โ</span> |
| </div> |
| </div> |
| <div class="batch-kf-kcard-ctrl"> |
| <label class="batch-kf-klabel">${escapeHtmlAttr(_t('batchStrength'))} |
| <input type="number" id="batch-kf-strength-${i}" value="${stVal}" min="0.1" max="1" step="0.01" |
| title="${escapeHtmlAttr(_t('batchStrengthTitle'))}"> |
| </label> |
| </div> |
| </div>`; |
| }); |
| container.innerHTML = ` |
| <div class="batch-kf-panel" id="batch-kf-timeline-root"> |
| <div class="batch-kf-panel-hd"> |
| <div class="batch-kf-panel-title">${escapeHtmlAttr(_t('batchKfPanelTitle'))}</div> |
| <div class="batch-kf-total-pill" title="${escapeHtmlAttr(_t('batchTotalPillTitle'))}"> |
| ${escapeHtmlAttr(_t('batchTotalDur'))} <strong id="batch-kf-total-seconds">โ</strong> <span class="batch-kf-total-unit">${escapeHtmlAttr(_t('batchTotalSec'))}</span> |
| </div> |
| </div> |
| <p class="batch-kf-panel-hint">${escapeHtmlAttr(_t('batchPanelHint'))}</p> |
| <div class="batch-kf-timeline-col"> |
| ${blocks} |
| </div> |
| </div>`; |
| updateBatchKfTimelineDerivedUI(); |
| return; |
| } |
| |
| let html = |
| '<div style="font-size: 12px; font-weight: bold; margin-bottom: 10px;">' + |
| escapeHtmlAttr(_t('batchSegTitle')) + |
| '</div>'; |
| |
| for (let i = 0; i < batchImages.length - 1; i++) { |
| const segPh = escapeHtmlAttr(_t('batchSegPromptPh')); |
| html += ` |
| <div style="background: var(--item); border-radius: 8px; padding: 10px; margin-bottom: 10px; border: 1px solid var(--border);"> |
| <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;"> |
| <div style="display: flex; align-items: center; gap: 8px;"> |
| <img src="${batchImages[i].preview}" style="width: 40px; height: 40px; border-radius: 4px; object-fit: cover;"> |
| <span style="color: var(--accent);">โ</span> |
| <img src="${batchImages[i + 1].preview}" style="width: 40px; height: 40px; border-radius: 4px; object-fit: cover;"> |
| <span style="font-size: 11px; color: var(--text-dim);">${escapeHtmlAttr(_t('batchSegClip'))} ${i + 1}</span> |
| </div> |
| <div style="display: flex; align-items: center; gap: 6px;"> |
| <label style="font-size: 10px; color: var(--text-dim);">${escapeHtmlAttr(_t('batchSegDuration'))}</label> |
| <input type="number" id="batch-segment-duration-${i}" value="5" min="1" max="30" step="1" style="width: 50px; padding: 4px; font-size: 11px;"> |
| <span style="font-size: 10px; color: var(--text-dim);">${escapeHtmlAttr(_t('batchSegSec'))}</span> |
| </div> |
| </div> |
| <div> |
| <label style="font-size: 10px;">${escapeHtmlAttr(_t('batchSegPrompt'))}</label> |
| <textarea id="batch-segment-prompt-${i}" placeholder="${segPh}" style="width: 100%; height: 60px; padding: 6px; font-size: 11px; box-sizing: border-box; resize: vertical;"></textarea> |
| </div> |
| </div> |
| `; |
| } |
| |
| container.innerHTML = html; |
| } |
|
|
| let _isGeneratingFlag = false; |
|
|
| |
| async function checkStatus() { |
| try { |
| const h = await fetch(`${BASE}/health`).then(r => r.json()).catch(() => ({status: "error"})); |
| const g = await fetch(`${BASE}/api/gpu-info`).then(r => r.json()).catch(() => ({gpu_info: {}})); |
| const p = await fetch(`${BASE}/api/generation/progress`).then(r => r.json()).catch(() => ({progress: 0})); |
| const sysGpus = await fetch(`${BASE}/api/system/list-gpus`).then(r => r.json()).catch(() => ({gpus: []})); |
| |
| const activeGpu = (sysGpus.gpus || []).find(x => x.active) || (sysGpus.gpus || [])[0] || {}; |
| const gpuName = activeGpu.name || g.gpu_info?.name || "GPU"; |
| |
| const s = document.getElementById('sys-status'); |
| const indicator = document.getElementById('sys-indicator'); |
| |
| const isReady = h.status === "ok" || h.status === "ready" || h.models_loaded; |
| const backendActive = (p && p.progress > 0); |
| |
| if (_isGeneratingFlag || backendActive) { |
| s.innerText = `${gpuName}: ${_t('sysBusy')}`; |
| if(indicator) indicator.className = 'indicator-busy'; |
| } else { |
| s.innerText = isReady ? `${gpuName}: ${_t('sysOnline')}` : `${gpuName}: ${_t('sysStarting')}`; |
| if(indicator) indicator.className = isReady ? 'indicator-ready' : 'indicator-offline'; |
| } |
| s.style.color = "var(--text-dim)"; |
|
|
| const vUsedMB = g.gpu_info?.vramUsed || 0; |
| const vTotalMB = activeGpu.vram_mb || g.gpu_info?.vram || 32768; |
| const vUsedGB = vUsedMB / 1024; |
| const vTotalGB = vTotalMB / 1024; |
| |
| document.getElementById('vram-fill').style.width = (vUsedMB / vTotalMB * 100) + "%"; |
| document.getElementById('vram-text').innerText = `${vUsedGB.toFixed(1)} / ${vTotalGB.toFixed(0)} GB`; |
| } catch(e) { document.getElementById('sys-status').innerText = _t('sysOffline'); } |
| } |
| setInterval(checkStatus, 1000); |
| checkStatus(); |
| initDragAndDrop(); |
| listGpus(); |
| |
|
|
| updateResPreview(); |
| updateBatchResPreview(); |
| updateImgResPreview(); |
| refreshPromptPlaceholder(); |
|
|
| window.onUiLanguageChanged = function () { |
| updateResPreview(); |
| updateBatchResPreview(); |
| updateImgResPreview(); |
| refreshPromptPlaceholder(); |
| if (typeof currentMode !== 'undefined' && currentMode === 'batch') { |
| updateBatchSegments(); |
| } |
| updateModelDropdown(); |
| updateLoraDropdown(); |
| updateBatchModelDropdown(); |
| updateBatchLoraDropdown(); |
| }; |
|
|
| async function setOutputDir() { |
| const dir = document.getElementById('global-out-dir').value.trim(); |
| localStorage.setItem('output_dir', dir); |
| try { |
| const res = await fetch(`${BASE}/api/system/set-dir`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ directory: dir }) |
| }); |
| if (res.ok) { |
| addLog(`โ
ๅญๅจ่ทฏๅพๆดๆฐๆๅ! ๅฝๅ่ทฏๅพ: ${dir || _t('defaultPath')}`); |
| if (typeof fetchHistory === 'function') fetchHistory(currentHistoryPage); |
| } |
| } catch (e) { |
| addLog(`โ ่ฎพ็ฝฎ่ทฏๅพๆถ่ฟๆฅๅผๅธธ: ${e.message}`); |
| } |
| } |
|
|
| async function browseOutputDir() { |
| try { |
| const res = await fetch(`${BASE}/api/system/browse-dir`); |
| const data = await res.json(); |
| if (data.status === "success" && data.directory) { |
| document.getElementById('global-out-dir').value = data.directory; |
| |
| setOutputDir(); |
| addLog(`๐ ๆฃๆตๅฐๆฐ่ทฏๅพ๏ผๅทฒ่ชๅจๅฅ็จ๏ผ`); |
| } else if (data.error) { |
| addLog(`โ ๅ
้จ็ณป็ปๆ้ๆฆๆชไบๅผน็ช: ${data.error}`); |
| } |
| } catch (e) { |
| addLog(`โ ๆ ๆณ่ฐๅบๆไปถๅคนๆต่งๅผน็ช, ่ฏท็ดๆฅๅคๅถ็ฒ่ดด็ปๅฏน่ทฏๅพใ`); |
| } |
| } |
|
|
| async function getOutputDir() { |
| try { |
| const res = await fetch(`${BASE}/api/system/get-dir`); |
| const data = await res.json(); |
| if (data.directory && data.directory.indexOf('LTXDesktop') === -1 && document.getElementById('global-out-dir')) { |
| document.getElementById('global-out-dir').value = data.directory; |
| } |
| } catch (e) {} |
| } |
|
|
| function switchMode(m) { |
| currentMode = m; |
| document.getElementById('tab-image').classList.toggle('active', m === 'image'); |
| document.getElementById('tab-video').classList.toggle('active', m === 'video'); |
| document.getElementById('tab-batch').classList.toggle('active', m === 'batch'); |
| document.getElementById('tab-upscale').classList.toggle('active', m === 'upscale'); |
| |
| document.getElementById('image-opts').style.display = m === 'image' ? 'block' : 'none'; |
| document.getElementById('video-opts').style.display = m === 'video' ? 'block' : 'none'; |
| document.getElementById('batch-opts').style.display = m === 'batch' ? 'block' : 'none'; |
| document.getElementById('upscale-opts').style.display = m === 'upscale' ? 'block' : 'none'; |
| if (m === 'batch') updateBatchSegments(); |
|
|
| |
| refreshPromptPlaceholder(); |
| } |
|
|
| function refreshPromptPlaceholder() { |
| const pe = document.getElementById('prompt'); |
| if (!pe) return; |
| pe.placeholder = |
| currentMode === 'upscale' ? _t('promptPlaceholderUpscale') : _t('promptPlaceholder'); |
| } |
|
|
| function showGeneratingView() { |
| if (!_isGeneratingFlag) return; |
| const resImg = document.getElementById('res-img'); |
| const videoWrapper = document.getElementById('video-wrapper'); |
| if (resImg) resImg.style.display = "none"; |
| if (videoWrapper) videoWrapper.style.display = "none"; |
| if (player) { |
| try { player.stop(); } catch(_) {} |
| } else { |
| const vid = document.getElementById('res-video'); |
| if (vid) { vid.pause(); vid.removeAttribute('src'); vid.load(); } |
| } |
| const loadingTxt = document.getElementById('loading-txt'); |
| if (loadingTxt) loadingTxt.style.display = "flex"; |
| } |
|
|
| async function run() { |
| |
| if (_isGeneratingFlag) { |
| addLog(_t('warnGenerating')); |
| return; |
| } |
|
|
| const btn = document.getElementById('mainBtn'); |
| const promptEl = document.getElementById('prompt'); |
| const prompt = promptEl ? promptEl.value.trim() : ''; |
|
|
| function batchHasUsablePrompt() { |
| if (prompt) return true; |
| const c = document.getElementById('batch-common-prompt')?.value?.trim(); |
| if (c) return true; |
| if (typeof batchWorkflowIsSingle === 'function' && batchWorkflowIsSingle()) { |
| return false; |
| } |
| if (batchImages.length < 2) return false; |
| for (let i = 0; i < batchImages.length - 1; i++) { |
| if (document.getElementById(`batch-segment-prompt-${i}`)?.value?.trim()) return true; |
| } |
| return false; |
| } |
|
|
| if (currentMode !== 'upscale') { |
| if (currentMode === 'batch') { |
| if (!batchHasUsablePrompt()) { |
| addLog(_t('warnBatchPrompt')); |
| return; |
| } |
| } else if (!prompt) { |
| addLog(_t('warnNeedPrompt')); |
| return; |
| } |
| } |
|
|
| if (!btn) { |
| console.error('mainBtn not found'); |
| return; |
| } |
|
|
| |
| _isGeneratingFlag = true; |
| btn.disabled = true; |
|
|
| try { |
| |
| const loader = document.getElementById('loading-txt'); |
| const resImg = document.getElementById('res-img'); |
| const resVideo = document.getElementById('res-video'); |
|
|
| if (loader) { |
| loader.style.display = "flex"; |
| loader.style.flexDirection = "column"; |
| loader.style.alignItems = "center"; |
| loader.style.gap = "12px"; |
| loader.innerHTML = ` |
| <div class="spinner" style="width:48px;height:48px;border-width:4px;color:var(--accent);"></div> |
| <div id="loader-step-text" style="font-size:13px;font-weight:700;color:var(--text-sub);">${escapeHtmlAttr(_t('loaderGpuAlloc'))}</div> |
| `; |
| } |
| if (resImg) resImg.style.display = "none"; |
| |
| const videoWrapper = document.getElementById('video-wrapper'); |
| if (videoWrapper) videoWrapper.style.display = "none"; |
| if (player) { try { player.stop(); } catch(_) {} } |
| else if (resVideo) { resVideo.pause?.(); resVideo.removeAttribute?.('src'); } |
|
|
| checkStatus(); |
|
|
| |
| try { await fetch(`${BASE}/api/system/reset-state`, { method: 'POST' }); } catch(_) {} |
|
|
| startProgressPolling(); |
|
|
| |
| const historyContainer = document.getElementById('history-container'); |
| if (historyContainer) { |
| const old = document.getElementById('current-loading-card'); |
| if (old) old.remove(); |
| const loadingCard = document.createElement('div'); |
| loadingCard.className = 'history-card loading-card'; |
| loadingCard.id = 'current-loading-card'; |
| loadingCard.onclick = showGeneratingView; |
| loadingCard.innerHTML = ` |
| <div class="spinner"></div> |
| <div id="loading-card-step" style="font-size:10px;color:var(--text-dim);margin-top:4px;">็ญๅพ
ไธญ...</div> |
| `; |
| historyContainer.prepend(loadingCard); |
| } |
|
|
| |
| let endpoint, payload; |
| if (currentMode === 'image') { |
| const w = parseInt(document.getElementById('img-w').value); |
| const h = parseInt(document.getElementById('img-h').value); |
| endpoint = '/api/generate-image'; |
| payload = { |
| prompt, width: w, height: h, |
| numSteps: parseInt(document.getElementById('img-steps').value), |
| numImages: 1 |
| }; |
| addLog(`ๆญฃๅจๅ่ตทๅพๅๆธฒๆ: ${w}x${h}, Steps: ${payload.numSteps}`); |
|
|
| } else if (currentMode === 'video') { |
| const res = updateResPreview(); |
| const dur = parseFloat(document.getElementById('vid-duration').value); |
| const fps = document.getElementById('vid-fps').value; |
| if (dur > 20) addLog(_t('warnVideoLong').replace('{n}', String(dur))); |
|
|
| const audio = document.getElementById('vid-audio').checked ? "true" : "false"; |
| const audioPath = document.getElementById('uploaded-audio-path').value; |
| const startFramePathValue = document.getElementById('start-frame-path').value; |
| const endFramePathValue = document.getElementById('end-frame-path').value; |
|
|
| let finalImagePath = null, finalStartFramePath = null, finalEndFramePath = null; |
| if (startFramePathValue && endFramePathValue) { |
| finalStartFramePath = startFramePathValue; |
| finalEndFramePath = endFramePathValue; |
| } else if (startFramePathValue) { |
| finalImagePath = startFramePathValue; |
| } |
|
|
| endpoint = '/api/generate'; |
| const modelSelect = document.getElementById('vid-model'); |
| const loraSelect = document.getElementById('vid-lora'); |
| const loraStrengthInput = document.getElementById('lora-strength'); |
| const modelPath = modelSelect ? modelSelect.value : ''; |
| const loraPath = loraSelect ? loraSelect.value : ''; |
| const loraStrength = loraStrengthInput ? (parseFloat(loraStrengthInput.value) || 1.0) : 1.0; |
| console.log("modelPath:", modelPath); |
| console.log("loraPath:", loraPath); |
| console.log("loraStrength:", loraStrength); |
| payload = { |
| prompt, resolution: res, model: "ltx-2", |
| cameraMotion: document.getElementById('vid-motion').value, |
| negativePrompt: "low quality, blurry, noisy, static noise, distorted", |
| duration: String(dur), fps, audio, |
| imagePath: finalImagePath, |
| audioPath: audioPath || null, |
| startFramePath: finalStartFramePath, |
| endFramePath: finalEndFramePath, |
| aspectRatio: document.getElementById('vid-ratio').value, |
| modelPath: modelPath || null, |
| loraPath: loraPath || null, |
| loraStrength: loraStrength, |
| }; |
| addLog(`ๆญฃๅจๅ่ตท่ง้ขๆธฒๆ: ${res}, ๆถ้ฟ: ${dur}s, FPS: ${fps}, ๆจกๅ: ${modelPath ? modelPath.split(/[/\\]/).pop() : _t('modelDefaultLabel')}, LoRA: ${loraPath ? loraPath.split(/[/\\]/).pop() : _t('loraNoneLabel')}`); |
|
|
| } else if (currentMode === 'upscale') { |
| const videoPath = document.getElementById('upscale-video-path').value; |
| const targetRes = document.getElementById('upscale-res').value; |
| if (!videoPath) throw new Error(_t('errUpscaleNoVideo')); |
| endpoint = '/api/system/upscale-video'; |
| payload = { video_path: videoPath, resolution: targetRes, prompt: "high quality, detailed, 4k", strength: 0.7 }; |
| addLog(`ๆญฃๅจๅ่ตท่ง้ข่ถ
ๅ: ็ฎๆ ${targetRes}`); |
| } else if (currentMode === 'batch') { |
| const res = updateBatchResPreview(); |
| const commonPromptEl = document.getElementById('batch-common-prompt'); |
| const commonPrompt = commonPromptEl ? commonPromptEl.value : ''; |
| const modelSelect = document.getElementById('batch-model'); |
| const loraSelect = document.getElementById('batch-lora'); |
| const loraStrengthInput = document.getElementById('batch-lora-strength'); |
| const modelPath = modelSelect ? modelSelect.value : ''; |
| const loraPath = loraSelect ? loraSelect.value : ''; |
| const loraStrength = loraStrengthInput ? (parseFloat(loraStrengthInput.value) || 1.2) : 1.2; |
| |
| if (batchImages.length < 2) { |
| throw new Error(_t('errBatchMinImages')); |
| } |
|
|
| if (batchWorkflowIsSingle()) { |
| captureBatchKfTimelineFromDom(); |
| const fps = document.getElementById('vid-fps').value; |
| const parts = [prompt.trim(), commonPrompt.trim()].filter(Boolean); |
| const combinedPrompt = parts.join(', '); |
| if (!combinedPrompt) { |
| throw new Error(_t('errSingleKfPrompt')); |
| } |
| const nKf = batchImages.length; |
| const minSeg = 0.1; |
| const segDurs = []; |
| for (let j = 0; j < nKf - 1; j++) { |
| let v = parseFloat(document.getElementById(`batch-kf-seg-dur-${j}`)?.value); |
| if (!Number.isFinite(v) || v < minSeg) v = minSeg; |
| segDurs.push(v); |
| } |
| const sumSec = segDurs.reduce((a, b) => a + b, 0); |
| const dur = Math.max(2, Math.ceil(sumSec - 1e-9)); |
| const times = [0]; |
| let acc = 0; |
| for (let j = 0; j < nKf - 1; j++) { |
| acc += segDurs[j]; |
| times.push(acc); |
| } |
| const strengths = []; |
| for (let i = 0; i < nKf; i++) { |
| const sEl = document.getElementById(`batch-kf-strength-${i}`); |
| let sv = parseFloat(sEl?.value); |
| if (!Number.isFinite(sv)) { |
| sv = parseFloat(defaultKeyframeStrengthForIndex(i, nKf)); |
| } |
| if (!Number.isFinite(sv)) sv = 1; |
| sv = Math.max(0.1, Math.min(1.0, sv)); |
| strengths.push(sv); |
| } |
| endpoint = '/api/generate'; |
| payload = { |
| prompt: combinedPrompt, |
| resolution: res, |
| model: "ltx-2", |
| cameraMotion: document.getElementById('vid-motion').value, |
| negativePrompt: "low quality, blurry, noisy, static noise, distorted", |
| duration: String(dur), |
| fps, |
| audio: "false", |
| imagePath: null, |
| audioPath: null, |
| startFramePath: null, |
| endFramePath: null, |
| keyframePaths: batchImages.map((b) => b.path), |
| keyframeStrengths: strengths, |
| keyframeTimes: times, |
| aspectRatio: document.getElementById('batch-ratio').value, |
| modelPath: modelPath || null, |
| loraPath: loraPath || null, |
| loraStrength: loraStrength, |
| }; |
| addLog( |
| `ๅๆฌกๅคๅ
ณ้ฎๅธง: ${nKf} ้็น, ่ฝด้ฟๅ่ฎก ${sumSec.toFixed(1)}s โ ่ฏทๆฑๆถ้ฟ ${dur}s, ${res}, FPS ${fps}` |
| ); |
| } else { |
| const segments = []; |
| for (let i = 0; i < batchImages.length - 1; i++) { |
| const duration = parseFloat(document.getElementById(`batch-segment-duration-${i}`)?.value || 5); |
| const segmentPrompt = document.getElementById(`batch-segment-prompt-${i}`)?.value || ''; |
| const segParts = [prompt.trim(), commonPrompt.trim(), segmentPrompt.trim()].filter(Boolean); |
| const combinedSegPrompt = segParts.join(', '); |
| segments.push({ |
| startImage: batchImages[i].path, |
| endImage: batchImages[i + 1].path, |
| duration: duration, |
| prompt: combinedSegPrompt |
| }); |
| } |
|
|
| endpoint = '/api/generate-batch'; |
| const bgAudioEl = document.getElementById('batch-background-audio-path'); |
| const backgroundAudioPath = (bgAudioEl && bgAudioEl.value) ? bgAudioEl.value.trim() : null; |
| payload = { |
| segments: segments, |
| resolution: res, |
| model: "ltx-2", |
| aspectRatio: document.getElementById('batch-ratio').value, |
| modelPath: modelPath || null, |
| loraPath: loraPath || null, |
| loraStrength: loraStrength, |
| negativePrompt: "low quality, blurry, noisy, static noise, distorted", |
| backgroundAudioPath: backgroundAudioPath || null |
| }; |
| addLog(`ๅๆฎตๆผๆฅ: ${segments.length} ๆฎต, ${res}${backgroundAudioPath ? '๏ผๅซ็ปไธ้
ไน' : ''}`); |
| } |
| } |
|
|
| |
| const res = await fetch(BASE + endpoint, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(payload) |
| }); |
| const data = await res.json(); |
| if (!res.ok) { |
| const errMsg = data.error || data.detail || "API ๆ็ปไบ่ฏทๆฑ"; |
| throw new Error(typeof errMsg === 'string' ? errMsg : JSON.stringify(errMsg)); |
| } |
|
|
| |
| const rawPath = data.image_paths ? data.image_paths[0] : data.video_path; |
| if (rawPath) { |
| try { displayOutput(rawPath); } catch (dispErr) { addLog(`โ ๏ธ ๆญๆพๅจๆพ็คบๅผๅธธ: ${dispErr.message}`); } |
| } |
|
|
| |
| setTimeout(() => { |
| isLoadingHistory = false; |
| if (typeof fetchHistory === 'function') fetchHistory(1); |
| }, 500); |
|
|
| } catch (e) { |
| const errText = e && e.message ? e.message : String(e); |
| addLog(`โ ๆธฒๆไธญๆญ: ${errText}`); |
| const loader = document.getElementById('loading-txt'); |
| if (loader) { |
| loader.style.display = 'flex'; |
| loader.textContent = ''; |
| const span = document.createElement('span'); |
| span.style.cssText = 'color:var(--text-sub);font-size:13px;padding:12px;text-align:center;'; |
| span.textContent = `ๆธฒๆๅคฑ่ดฅ๏ผ${errText}`; |
| loader.appendChild(span); |
| } |
|
|
| } finally { |
| |
| _isGeneratingFlag = false; |
| btn.disabled = false; |
| stopProgressPolling(); |
| checkStatus(); |
| |
| setTimeout(() => { clearGpu(); }, 500); |
| } |
| } |
|
|
| async function clearGpu() { |
| const btn = document.getElementById('clearGpuBtn'); |
| btn.disabled = true; |
| btn.innerText = _t('clearingVram'); |
| try { |
| const res = await fetch(`${BASE}/api/system/clear-gpu`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' } |
| }); |
| const data = await res.json(); |
| if (res.ok) { |
| addLog(`๐งน ๆพๅญๆธ
็ๆๅ: ${data.message}`); |
| |
| checkStatus(); |
| setTimeout(checkStatus, 1000); |
| } else { |
| const errMsg = data.error || data.detail || "ๅ็ซฏๆชๅฎ็ฐๆญคๆฅๅฃ (404)"; |
| throw new Error(errMsg); |
| } |
| } catch(e) { |
| addLog(`โ ๆธ
็ๆพๅญๅคฑ่ดฅ: ${e.message}`); |
| } finally { |
| btn.disabled = false; |
| btn.innerText = _t('clearVram'); |
| } |
| } |
|
|
| async function listGpus() { |
| try { |
| const res = await fetch(`${BASE}/api/system/list-gpus`); |
| const data = await res.json(); |
| if (res.ok && data.gpus) { |
| const selector = document.getElementById('gpu-selector'); |
| selector.innerHTML = data.gpus.map(g => |
| `<option value="${g.id}" ${g.active ? 'selected' : ''}>GPU ${g.id}: ${g.name} (${g.vram})</option>` |
| ).join(''); |
| |
| |
| const activeGpu = data.gpus.find(g => g.active); |
| if (activeGpu) document.getElementById('gpu-name').innerText = activeGpu.name; |
| } |
| } catch (e) { |
| console.error("Failed to list GPUs", e); |
| } |
| } |
|
|
| async function switchGpu(id) { |
| if (!id) return; |
| addLog(`๐ ๆญฃๅจๅๆขๅฐ GPU ${id}...`); |
| try { |
| const res = await fetch(`${BASE}/api/system/switch-gpu`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ gpu_id: parseInt(id) }) |
| }); |
| const data = await res.json(); |
| if (res.ok) { |
| addLog(`โ
ๅทฒๆๅๅๆขๅฐ GPU ${id}๏ผๆจกๅๅฐ้ๆฐๅ ่ฝฝใ`); |
| listGpus(); |
| setTimeout(checkStatus, 1000); |
| } else { |
| throw new Error(data.error || "ๅๆขๅคฑ่ดฅ"); |
| } |
| } catch (e) { |
| addLog(`โ GPU ๅๆขๅคฑ่ดฅ: ${e.message}`); |
| } |
| } |
|
|
| function startProgressPolling() { |
| if (pollInterval) clearInterval(pollInterval); |
| pollInterval = setInterval(async () => { |
| try { |
| const res = await fetch(`${BASE}/api/generation/progress`); |
| const d = await res.json(); |
| if (d.progress > 0) { |
| const ph = String(d.phase || 'inference'); |
| const phaseKey = 'phase_' + ph; |
| let phaseStr = _t(phaseKey); |
| if (phaseStr === phaseKey) phaseStr = ph; |
|
|
| let stepLabel; |
| if (d.current_step !== undefined && d.current_step !== null && d.total_steps) { |
| stepLabel = `${d.current_step}/${d.total_steps} ${_t('progressStepUnit')}`; |
| } else { |
| stepLabel = `${d.progress}%`; |
| } |
|
|
| document.getElementById('progress-fill').style.width = d.progress + "%"; |
| const loaderStep = document.getElementById('loader-step-text'); |
| const busyLine = `${_t('gpuBusyPrefix')}: ${stepLabel} [${phaseStr}]`; |
| if (loaderStep) loaderStep.innerText = busyLine; |
| else { |
| const loadingTxt = document.getElementById('loading-txt'); |
| if (loadingTxt) loadingTxt.innerText = busyLine; |
| } |
|
|
| |
| const cardStep = document.getElementById('loading-card-step'); |
| if (cardStep) cardStep.innerText = stepLabel; |
| } |
| } catch(e) {} |
| }, 1000); |
| } |
|
|
| function stopProgressPolling() { |
| clearInterval(pollInterval); |
| pollInterval = null; |
| document.getElementById('progress-fill').style.width = "0%"; |
| |
| const lc = document.getElementById('current-loading-card'); |
| if (lc) lc.remove(); |
| } |
|
|
| function displayOutput(fileOrPath) { |
| const img = document.getElementById('res-img'); |
| const vid = document.getElementById('res-video'); |
| const loader = document.getElementById('loading-txt'); |
| |
| |
| if(player) { |
| player.stop(); |
| } else { |
| vid.pause(); |
| vid.removeAttribute('src'); |
| vid.load(); |
| } |
| |
| let url = ""; |
| let fileName = fileOrPath; |
| if (fileOrPath.indexOf('\\') !== -1 || fileOrPath.indexOf('/') !== -1) { |
| url = `${BASE}/api/system/file?path=${encodeURIComponent(fileOrPath)}&t=${Date.now()}`; |
| fileName = fileOrPath.split(/[\\/]/).pop(); |
| } else { |
| const outInput = document.getElementById('global-out-dir'); |
| const globalDir = outInput ? outInput.value.replace(/\\/g, '/').replace(/\/$/, '') : ""; |
| if (globalDir && globalDir !== "") { |
| url = `${BASE}/api/system/file?path=${encodeURIComponent(globalDir + '/' + fileOrPath)}&t=${Date.now()}`; |
| } else { |
| url = `${BASE}/outputs/${fileOrPath}?t=${Date.now()}`; |
| } |
| } |
|
|
| loader.style.display = "none"; |
| if (currentMode === 'image') { |
| img.src = url; |
| img.style.display = "block"; |
| addLog(`โ
ๅพๅๆธฒๆๆๅ: ${fileName}`); |
| } else { |
| document.getElementById('video-wrapper').style.display = "flex"; |
| |
| if(player) { |
| player.source = { |
| type: 'video', |
| sources: [{ src: url, type: 'video/mp4' }] |
| }; |
| player.play(); |
| } else { |
| vid.src = url; |
| } |
| addLog(`โ
่ง้ขๆธฒๆๆๅ: ${fileName}`); |
| } |
| } |
|
|
|
|
|
|
| function addLog(msg) { |
| const log = document.getElementById('log'); |
| if (!log) { |
| console.log('[LTX]', msg); |
| return; |
| } |
| const time = new Date().toLocaleTimeString(); |
| log.innerHTML += `<div style="margin-bottom:5px"> <span style="color:var(--text-dim)">[${time}]</span> ${msg}</div>`; |
| log.scrollTop = log.scrollHeight; |
| } |
|
|
|
|
| |
| window.addEventListener('DOMContentLoaded', () => switchMode('video')); |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let currentHistoryPage = 1; |
| let isLoadingHistory = false; |
| |
| let _historyListFingerprint = ''; |
|
|
| function switchLibTab(tab) { |
| document.getElementById('log-container').style.display = tab === 'log' ? 'flex' : 'none'; |
| const hw = document.getElementById('history-wrapper'); |
| if (hw) hw.style.display = tab === 'history' ? 'block' : 'none'; |
| |
| document.getElementById('tab-log').style.color = tab === 'log' ? 'var(--accent)' : 'var(--text-dim)'; |
| document.getElementById('tab-log').style.borderColor = tab === 'log' ? 'var(--accent)' : 'transparent'; |
| |
| document.getElementById('tab-history').style.color = tab === 'history' ? 'var(--accent)' : 'var(--text-dim)'; |
| document.getElementById('tab-history').style.borderColor = tab === 'history' ? 'var(--accent)' : 'transparent'; |
| |
| if (tab === 'history') { |
| fetchHistory(); |
| } |
| } |
|
|
| async function fetchHistory(isFirstLoad = false, silent = false) { |
| if (isLoadingHistory) return; |
| isLoadingHistory = true; |
| |
| try { |
| |
| const res = await fetch(`${BASE}/api/system/history?page=1&limit=10000`); |
| if (!res.ok) { |
| isLoadingHistory = false; |
| return; |
| } |
| const data = await res.json(); |
|
|
| const validHistory = (data.history || []).filter(item => item && item.filename); |
| const fingerprint = validHistory.length === 0 |
| ? '__empty__' |
| : validHistory.map(h => `${h.type}|${h.filename}`).join('\0'); |
|
|
| if (silent && fingerprint === _historyListFingerprint) { |
| return; |
| } |
|
|
| const container = document.getElementById('history-container'); |
| if (!container) { |
| return; |
| } |
|
|
| let loadingCardHtml = ""; |
| const lc = document.getElementById('current-loading-card'); |
| if (lc && _isGeneratingFlag) { |
| loadingCardHtml = lc.outerHTML; |
| } |
|
|
| if (validHistory.length === 0) { |
| container.innerHTML = loadingCardHtml; |
| const newLcEmpty = document.getElementById('current-loading-card'); |
| if (newLcEmpty) newLcEmpty.onclick = showGeneratingView; |
| _historyListFingerprint = fingerprint; |
| return; |
| } |
|
|
| container.innerHTML = loadingCardHtml; |
|
|
| const outInput = document.getElementById('global-out-dir'); |
| const globalDir = outInput ? outInput.value.replace(/\\/g, '/').replace(/\/$/, '') : ""; |
|
|
| const cardsHtml = validHistory.map((item, index) => { |
| const url = (globalDir && globalDir !== "") |
| ? `${BASE}/api/system/file?path=${encodeURIComponent(globalDir + '/' + item.filename)}` |
| : `${BASE}/outputs/${item.filename}`; |
| |
| const safeFilename = item.filename.replace(/'/g, "\\'").replace(/"/g, '\\"'); |
| const media = item.type === 'video' |
| ? `<video data-src="${url}#t=0.001" class="lazy-load history-thumb-media" muted loop preload="none" playsinline onmouseover="if(this.readyState >= 2) this.play()" onmouseout="this.pause()" style="pointer-events: none; object-fit: cover; width: 100%; height: 100%;"></video>` |
| : `<img data-src="${url}" class="lazy-load history-thumb-media" alt="" style="object-fit: cover; width: 100%; height: 100%;">`; |
| return `<div class="history-card" onclick="displayHistoryOutput('${safeFilename}', '${item.type}')"> |
| <div class="history-type-badge">${item.type === 'video' ? '๐ฌ VID' : '๐จ IMG'}</div> |
| <button class="history-delete-btn" onclick="event.stopPropagation(); deleteHistoryItem('${safeFilename}', '${item.type}', this)">โ</button> |
| ${media} |
| </div>`; |
| }).join(''); |
|
|
| container.insertAdjacentHTML('beforeend', cardsHtml); |
|
|
| |
| const newLc = document.getElementById('current-loading-card'); |
| if (newLc) newLc.onclick = showGeneratingView; |
|
|
| |
| loadVisibleImages(); |
| _historyListFingerprint = fingerprint; |
| } catch(e) { |
| console.error("Failed to load history", e); |
| } finally { |
| isLoadingHistory = false; |
| } |
| } |
| |
| async function deleteHistoryItem(filename, type, btn) { |
| if (!confirm(`็กฎๅฎ่ฆๅ ้ค "${filename}" ๅ๏ผ`)) return; |
| |
| try { |
| const res = await fetch(`${BASE}/api/system/delete-file`, { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({filename: filename, type: type}) |
| }); |
| |
| if (res.ok) { |
| |
| const card = btn.closest('.history-card'); |
| if (card) { |
| card.remove(); |
| } |
| } else { |
| alert('ๅ ้คๅคฑ่ดฅ'); |
| } |
| } catch(e) { |
| console.error('Delete failed', e); |
| alert('ๅ ้คๅคฑ่ดฅ'); |
| } |
| } |
|
|
| function loadVisibleImages() { |
| const hw = document.getElementById('history-wrapper'); |
| if (!hw) return; |
| |
| const lazyMedias = document.querySelectorAll('#history-container .lazy-load'); |
| |
| |
| let loadedCount = 0; |
| lazyMedias.forEach(media => { |
| if (loadedCount >= 3) return; |
| |
| const src = media.dataset.src; |
| if (!src) return; |
| |
| |
| const rect = media.getBoundingClientRect(); |
| const containerRect = hw.getBoundingClientRect(); |
| |
| if (rect.top < containerRect.bottom + 300 && rect.bottom > containerRect.top - 100) { |
| let revealed = false; |
| let thumbRevealTimer; |
| const revealThumb = () => { |
| if (revealed) return; |
| revealed = true; |
| if (thumbRevealTimer) clearTimeout(thumbRevealTimer); |
| media.classList.add('history-thumb-ready'); |
| }; |
| thumbRevealTimer = setTimeout(revealThumb, 4000); |
|
|
| if (media.tagName === 'VIDEO') { |
| media.addEventListener('loadeddata', revealThumb, { once: true }); |
| media.addEventListener('error', revealThumb, { once: true }); |
| } else { |
| media.addEventListener('load', revealThumb, { once: true }); |
| media.addEventListener('error', revealThumb, { once: true }); |
| } |
|
|
| media.src = src; |
| media.classList.remove('lazy-load'); |
|
|
| if (media.tagName === 'VIDEO') { |
| media.preload = 'metadata'; |
| if (media.readyState >= 2) revealThumb(); |
| } else if (media.complete && media.naturalWidth > 0) { |
| revealThumb(); |
| } |
|
|
| loadedCount++; |
| } |
| }); |
| |
| |
| if (loadedCount > 0) { |
| setTimeout(loadVisibleImages, 100); |
| } |
| } |
|
|
| |
| function initHistoryScrollListener() { |
| const hw = document.getElementById('history-wrapper'); |
| if (!hw) return; |
| |
| let scrollTimeout; |
| hw.addEventListener('scroll', () => { |
| if (scrollTimeout) clearTimeout(scrollTimeout); |
| scrollTimeout = setTimeout(() => { |
| loadVisibleImages(); |
| }, 100); |
| }); |
| } |
|
|
| |
| window.addEventListener('DOMContentLoaded', () => { |
| setTimeout(initHistoryScrollListener, 500); |
| }); |
|
|
| function displayHistoryOutput(file, type) { |
| document.getElementById('res-img').style.display = 'none'; |
| document.getElementById('video-wrapper').style.display = 'none'; |
|
|
| const mode = type === 'video' ? 'video' : 'image'; |
| switchMode(mode); |
| displayOutput(file); |
| } |
| |
| window.addEventListener('DOMContentLoaded', () => { |
| |
| if(window.Plyr) { |
| player = new Plyr('#res-video', { |
| controls: [ |
| 'play-large', 'play', 'progress', 'current-time', |
| 'mute', 'volume', 'fullscreen' |
| ], |
| settings: [], |
| loop: { active: true }, |
| autoplay: true |
| }); |
| } |
| |
| |
| fetch(`${BASE}/api/system/get-dir`) |
| .then((res) => res.json()) |
| .then((data) => { |
| if (data && data.directory) { |
| const outInput = document.getElementById('global-out-dir'); |
| if (outInput) outInput.value = data.directory; |
| } |
| }) |
| .catch((e) => console.error(e)) |
| .finally(() => { |
| |
| switchLibTab('history'); |
| }); |
|
|
| let historyRefreshInterval = null; |
| function startHistoryAutoRefresh() { |
| if (historyRefreshInterval) return; |
| historyRefreshInterval = setInterval(() => { |
| const hc = document.getElementById('history-container'); |
| if (hc && hc.offsetParent !== null && !_isGeneratingFlag) { |
| fetchHistory(1, true); |
| } |
| }, 5000); |
| } |
| startHistoryAutoRefresh(); |
| }); |