diff --git "a/LTX2.3-1.0.4-new/UI/index.js" "b/LTX2.3-1.0.4-new/UI/index.js"
deleted file mode 100644--- "a/LTX2.3-1.0.4-new/UI/index.js"
+++ /dev/null
@@ -1,3312 +0,0 @@
-// ─── Resizable panel drag logic ───────────────────────────────────────────────
-(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;
- // Clamp: viewer min 150px, library min 100px
- 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)';
- }
- });
- // Hover highlight
- handle.addEventListener('mouseenter', () => { handle.querySelector('div').style.background = 'var(--text-dim)'; });
- handle.addEventListener('mouseleave', () => { if (!dragging) handle.querySelector('div').style.background = 'var(--border)'; });
-})();
-// ──────────────────────────────────────────────────────────────────────────────
-
-
-
-
-
-
-// 动态获取当前访问的域名或 IP,自动对齐 3000 端口
- const BASE = `http://${window.location.hostname}:3000`;
-
- function _t(k) {
- return typeof window.t === 'function' ? window.t(k) : k;
- }
-
- function _fmt(key, vars) {
- let text = _t(key);
- Object.entries(vars || {}).forEach(([name, value]) => {
- text = text.replaceAll(`{${name}}`, String(value));
- });
- return text;
- }
-
- let currentMode = 'image';
- let pollInterval = null;
- let availableLoras = [];
- let player = null;
- let audioPlayer = null;
- const MODEL_CHECKPOINT_STORAGE_KEY = 'ltx_selected_model_path';
-
- // 建议增加一个简单的调试日志,方便在控制台确认地址是否正确
- console.log("Connecting to Backend API at:", BASE);
-
- // LoRA 扫描功能
- 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}`);
- }
- }
-
- window.addLoraSelection = function(containerId) {
- const container = document.getElementById(containerId);
- if (!container) return;
-
- const wrapper = document.createElement('div');
- wrapper.className = 'lora-entry';
- wrapper.style.display = 'flex';
- wrapper.style.flexDirection = 'column';
- wrapper.style.gap = '4px';
- wrapper.style.padding = '8px';
- wrapper.style.background = 'rgba(255,255,255,0.03)';
- wrapper.style.borderRadius = '6px';
- wrapper.style.border = '1px solid var(--border)';
-
- const row1 = document.createElement('div');
- row1.style.display = 'flex';
- row1.style.gap = '8px';
- row1.style.alignItems = 'center';
-
- const select = document.createElement('select');
- select.className = 'lora-select';
- select.style.flex = '1';
- select.innerHTML = '';
- availableLoras.forEach(lora => {
- const opt = document.createElement('option');
- opt.value = lora.path;
- opt.textContent = lora.name;
- select.appendChild(opt);
- });
-
- const removeBtn = document.createElement('button');
- removeBtn.type = 'button';
- removeBtn.title = '移除';
- removeBtn.innerHTML = '×';
- removeBtn.style.background = 'none';
- removeBtn.style.border = 'none';
- removeBtn.style.color = 'var(--text-dim)';
- removeBtn.style.cursor = 'pointer';
- removeBtn.style.fontSize = '18px';
- removeBtn.style.padding = '0 6px';
- removeBtn.onclick = () => { wrapper.remove(); };
-
- row1.appendChild(select);
- row1.appendChild(removeBtn);
-
- const strengthContainer = document.createElement('div');
- strengthContainer.className = 'lora-strength-container';
- strengthContainer.style.display = 'none';
- strengthContainer.style.alignItems = 'center';
- strengthContainer.style.gap = '8px';
- strengthContainer.style.marginTop = '4px';
-
- const strLabel = document.createElement('label');
- strLabel.textContent = _t('loraStrength') || 'LoRA 强度';
- strLabel.style.margin = '0';
- strLabel.style.whiteSpace = 'nowrap';
- strLabel.style.fontSize = '11px';
-
- const strInput = document.createElement('input');
- strInput.type = 'range';
- strInput.className = 'lora-strength';
- strInput.min = '0.1';
- strInput.max = '2.0';
- strInput.step = '0.1';
- strInput.value = '2.0';
- strInput.style.flex = '1';
- strInput.style.margin = '0';
-
- const strVal = document.createElement('span');
- strVal.className = 'lora-strength-val';
- strVal.textContent = '2.0';
- strVal.style.fontSize = '11px';
- strVal.style.color = 'var(--accent)';
- strVal.style.width = '24px';
- strVal.style.textAlign = 'right';
-
- strInput.oninput = () => { strVal.textContent = strInput.value; };
-
- strengthContainer.appendChild(strLabel);
- strengthContainer.appendChild(strInput);
- strengthContainer.appendChild(strVal);
-
- wrapper.appendChild(row1);
- wrapper.appendChild(strengthContainer);
-
- select.onchange = () => {
- strengthContainer.style.display = select.value ? 'flex' : 'none';
- };
-
- container.appendChild(wrapper);
- };
-
- function updateLoraDropdown() {
- const selects = document.querySelectorAll('#loras-container .lora-select');
- selects.forEach(select => {
- const currentVal = select.value;
- select.innerHTML = '';
- availableLoras.forEach(lora => {
- const opt = document.createElement('option');
- opt.value = lora.path;
- opt.textContent = lora.name;
- select.appendChild(opt);
- });
- select.value = currentVal;
- });
- if (document.getElementById('loras-container') && document.getElementById('loras-container').children.length === 0) {
- window.addLoraSelection('loras-container');
- }
- }
-
- window.updateLoraStrength = function() {};
-
- function updateBatchLoraDropdown() {
- const selects = document.querySelectorAll('#batch-loras-container .lora-select');
- selects.forEach(select => {
- const currentVal = select.value;
- select.innerHTML = '';
- availableLoras.forEach(lora => {
- const opt = document.createElement('option');
- opt.value = lora.path;
- opt.textContent = lora.name;
- select.appendChild(opt);
- });
- select.value = currentVal;
- });
- if (document.getElementById('batch-loras-container') && document.getElementById('batch-loras-container').children.length === 0) {
- window.addLoraSelection('batch-loras-container');
- }
- }
-
- window.updateBatchLoraStrength = function() {};
-
- // 页面加载时更新批量模式的下拉框
- function initBatchDropdowns() {
- updateBatchLoraDropdown();
- }
-
- // 已移除:模型/LoRA 目录自定义与浏览(保持后端默认路径扫描)
-
- // 页面加载时扫描 LoRA(模型选择入口暂时隐藏,后端保留待后续调试)
- (function() {
- ['vid-quality', 'batch-quality'].forEach((id) => {
- const sel = document.getElementById(id);
- if (sel && sel.value === '544') sel.value = '540';
- });
-
- setTimeout(() => {
- scanLoras();
- initBatchDropdowns();
- }, 1500);
- })();
-
- // 分辨率自动计算逻辑
- const uploadedFrameDims = { start: null, end: null };
-
- function roundTo64(value) {
- const n = Math.round(Number(value || 0) / 64) * 64;
- return Math.max(64, n);
- }
-
- function parseRatioValue(value) {
- const [a, b] = String(value || '16:9').split(':').map(Number);
- if (!a || !b) return 16 / 9;
- return a / b;
- }
-
- function sizeForRatio(quality, ratioValue) {
- const shortSide = quality === '1080' ? 1088 : quality === '720' ? 704 : 576;
- const ratio = parseRatioValue(ratioValue);
- if (ratio >= 1) return { width: roundTo64(shortSide * ratio), height: roundTo64(shortSide) };
- return { width: roundTo64(shortSide), height: roundTo64(shortSide / ratio) };
- }
-
- function getReferenceSize(kind) {
- if (kind === 'batch') {
- return batchImages?.[0]?.width && batchImages?.[0]?.height
- ? { width: batchImages[0].width, height: batchImages[0].height }
- : null;
- }
- return uploadedFrameDims.start;
- }
-
- function getGenerationSize(kind) {
- const isBatch = kind === 'batch';
- const q = document.getElementById(isBatch ? 'batch-quality' : 'vid-quality').value;
- const ratioEl = document.getElementById(isBatch ? 'batch-ratio' : 'vid-ratio');
- const ratio = ratioEl?.value || '16:9';
- const customBox = document.getElementById(isBatch ? 'batch-custom-size' : 'vid-custom-size');
- if (customBox) customBox.style.display = ratio === 'custom' ? 'flex' : 'none';
-
- if (ratio === 'custom') {
- const w = document.getElementById(isBatch ? 'batch-custom-w' : 'vid-custom-w')?.value;
- const h = document.getElementById(isBatch ? 'batch-custom-h' : 'vid-custom-h')?.value;
- return { width: roundTo64(w), height: roundTo64(h), source: 'custom' };
- }
- if (ratio === 'ref') {
- const ref = getReferenceSize(kind);
- if (ref) return { width: roundTo64(ref.width), height: roundTo64(ref.height), source: 'ref' };
- return { ...sizeForRatio(q, '16:9'), source: 'ref-missing' };
- }
- return { ...sizeForRatio(q, ratio), source: ratio };
- }
-
- function resLabelFromQuality(q) {
- return q === "1080" ? "1080p" : q === "720" ? "720p" : "540p";
- }
-
- function updateResPreview() {
- const q = document.getElementById('vid-quality').value; // "1080", "720", "540"
- const size = getGenerationSize('video');
- const note = size.source === 'ref-missing' ? ` · ${_t('ratioRefMissing')}` : '';
- document.getElementById('res-preview').innerText = `${_t('resPreviewPrefix')}: ${resLabelFromQuality(q)} (${size.width}x${size.height})${note}`;
- return resLabelFromQuality(q);
- }
-
- // 图片分辨率预览
- 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 size = getGenerationSize('batch');
- const note = size.source === 'ref-missing' ? ` · ${_t('ratioRefMissing')}` : '';
- document.getElementById('batch-res-preview').innerText = `${_t('resPreviewPrefix')}: ${resLabelFromQuality(q)} (${size.width}x${size.height})${note}`;
- return resLabelFromQuality(q);
- }
-
- // 批量模式 LoRA 强度切换
- 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();
- // 隐藏自定义区域或保持显示供微调
- // document.getElementById('img-custom-res').style.display = 'none';
- }
- }
-
-
-
- // 处理帧图片上传
- function getImageDataUrlSize(dataUrl) {
- return new Promise((resolve) => {
- const probe = new Image();
- probe.onload = () => resolve({ width: probe.naturalWidth || probe.width, height: probe.naturalHeight || probe.height });
- probe.onerror = () => resolve(null);
- probe.src = dataUrl;
- });
- }
-
- 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 = async (e) => {
- preview.src = e.target.result;
- preview.style.display = 'block';
- placeholder.style.display = 'none';
- clearOverlay.style.display = 'flex';
- uploadedFrameDims[frameType] = await getImageDataUrlSize(e.target.result);
- updateResPreview();
- };
- 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 = "";
- uploadedFrameDims[frameType] = null;
- 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' ? '起始帧' : '结束帧'}`);
- updateResPreview();
- }
-
- // 处理图片上传
- 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);
-
- // 使用 FileReader 转换为 Base64,绕过后端缺失 python-multipart 的问题
- 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 uploadBase64File(file, logLabel) {
- const b64Data = await new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = (e) => resolve(e.target.result);
- reader.onerror = () => reject(new Error(_t('fileReadFail')));
- reader.readAsDataURL(file);
- });
- addLog(_fmt('uploadFileStart', { label: logLabel, name: file.name }));
- 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().catch(() => ({}));
- if (!res.ok || !data.path) {
- const errMsg = data.error || data.detail || "上传失败";
- throw new Error(typeof errMsg === 'string' ? errMsg : JSON.stringify(errMsg));
- }
- return data.path;
- }
-
- window.handleMotionVideoUpload = async function(file) {
- if (!file) return;
- const label = _t('motionRefVideoName');
- try {
- const path = await uploadBase64File(file, label);
- document.getElementById('motion-video-path').value = path;
- document.getElementById('motion-video-placeholder').style.display = 'none';
- document.getElementById('motion-video-status').style.display = 'block';
- document.getElementById('motion-video-name').textContent = file.name;
- document.getElementById('clear-motion-video-overlay').style.display = 'flex';
- addLog(_fmt('motionUploadOk', { label, name: file.name }));
- } catch (e) {
- addLog(_fmt('motionUploadFail', { label, message: e.message }));
- }
- };
-
- window.clearMotionVideo = function() {
- document.getElementById('motion-video-input').value = "";
- document.getElementById('motion-video-path').value = "";
- document.getElementById('motion-video-placeholder').style.display = 'block';
- document.getElementById('motion-video-status').style.display = 'none';
- document.getElementById('motion-video-name').textContent = "";
- document.getElementById('clear-motion-video-overlay').style.display = 'none';
- addLog(_t('motionClearRefVideo'));
- };
-
- window.handleMotionImageUpload = async function(file) {
- if (!file) return;
- const preview = document.getElementById('motion-image-preview');
- const placeholder = document.getElementById('motion-image-placeholder');
- const clearOverlay = document.getElementById('clear-motion-image-overlay');
- const reader = new FileReader();
- reader.onload = (e) => {
- preview.src = e.target.result;
- preview.style.display = 'block';
- placeholder.style.display = 'none';
- clearOverlay.style.display = 'flex';
- };
- reader.readAsDataURL(file);
- const label = _t('motionTargetImageName');
- try {
- const path = await uploadBase64File(file, label);
- document.getElementById('motion-image-path').value = path;
- addLog(_fmt('motionUploadOk', { label, name: file.name }));
- } catch (e) {
- addLog(_fmt('motionUploadFail', { label, message: e.message }));
- }
- };
-
- window.clearMotionImage = function() {
- document.getElementById('motion-image-input').value = "";
- document.getElementById('motion-image-path').value = "";
- document.getElementById('motion-image-preview').style.display = 'none';
- document.getElementById('motion-image-preview').src = "";
- document.getElementById('motion-image-placeholder').style.display = 'block';
- document.getElementById('clear-motion-image-overlay').style.display = 'none';
- addLog(_t('motionClearTargetImage'));
- };
-
- // 初始化拖拽上传逻辑
- 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 batchImagesDropZone = document.getElementById('batch-images-drop-zone');
- const motionVideoDropZone = document.getElementById('motion-video-drop-zone');
- const motionImageDropZone = document.getElementById('motion-image-drop-zone');
- const ttsRefDropZone = document.getElementById('tts-ref-drop');
-
- const zones = [audioDropZone, startFrameDropZone, endFrameDropZone, batchImagesDropZone, motionVideoDropZone, motionImageDropZone, ttsRefDropZone].filter(z => z);
-
- ['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);
-
- // 批量图片拖拽上传
- 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);
- }
-
- if (motionVideoDropZone) {
- motionVideoDropZone.addEventListener('drop', (e) => {
- const file = e.dataTransfer.files[0];
- if (file && (file.type.startsWith('video/') || /\.(mp4|mov|webm|mkv|avi)$/i.test(file.name))) {
- window.handleMotionVideoUpload(file);
- }
- }, false);
- }
-
- if (motionImageDropZone) {
- motionImageDropZone.addEventListener('drop', (e) => {
- const file = e.dataTransfer.files[0];
- if (file && file.type.startsWith('image/')) {
- window.handleMotionImageUpload(file);
- }
- }, false);
- }
-
- if (ttsRefDropZone) {
- ttsRefDropZone.addEventListener('drop', (e) => {
- const file = e.dataTransfer.files[0];
- if (file && (file.type.startsWith('audio/') || file.name.endsWith('.wav') || file.name.endsWith('.mp3'))) {
- handleTtsRefUpload(file);
- }
- }, false);
- }
- }
-
- // 批量图片上传处理
- let batchImages = [];
- /** 单次多关键帧:按 path 记引导强度;按帧索引记每张图占用秒数 */
- const batchKfStrengthByPath = {};
- const batchKfSegDurByIndex = {};
-
- function escapeHtmlAttr(s) {
- return String(s)
- .replace(/&/g, '&')
- .replace(/"/g, '"')
- .replace(/ {
- 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; j++) {
- const el = document.getElementById(`batch-kf-seg-dur-${j}`);
- if (el) batchKfSegDurByIndex[j] = el.value.trim();
- }
- }
-
- /** 读取每帧占用时长(秒),非法则回退为 minSeg */
- function readBatchKfFrameSeconds(n, minSeg) {
- const secs = [];
- for (let j = 0; j < n; j++) {
- let v = parseFloat(document.getElementById(`batch-kf-seg-dur-${j}`)?.value);
- if (!Number.isFinite(v) || v < minSeg) v = minSeg;
- secs.push(v);
- }
- return secs;
- }
-
- function updateBatchKfTimelineDerivedUI() {
- if (!batchWorkflowIsSingle() || batchImages.length < 2) return;
- const n = batchImages.length;
- const minSeg = 0.1;
- const frameSecs = readBatchKfFrameSeconds(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 += frameSecs[i - 1];
- label.textContent =
- i === n - 1
- ? `${t.toFixed(1)} s · ${_t('batchAnchorLast')}`
- : `${t.toFixed(1)} s`;
- }
- }
- const totalEl = document.getElementById('batch-kf-total-seconds');
- if (totalEl) {
- const sum = frameSecs.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) {
- const dims = await getImageDataUrlSize(e.target.result);
- resolve({ name: file.name, path: data.path, preview: e.target.result, width: dims?.width || null, height: dims?.height || null });
- } else {
- resolve(null);
- }
- } catch (e) {
- resolve(null);
- }
- };
- reader.readAsDataURL(file);
- });
-
- if (imgData) {
- batchImages.push(imgData);
- addLog(`✅ 图片 ${i + 1}/${files.length} 上传成功: ${file.name}`);
- }
- }
-
- renderBatchImages();
- updateBatchSegments();
- updateBatchResPreview();
- }
-
- 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 = '' + _t('batchAddMore') + '';
- 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);
- /* 不在 pointerdown 立刻重算槽位;双 rAF 后再恢复邻居 transition,保证先完成本帧布局再动画 */
- 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();
- }
-
- // 横向缩略图:Pointer 拖动排序(避免 HTML5 DnD 在 WebView/部分浏览器失效)
- 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();
- }
-
- // 更新片段设置 UI(分段模式)或单次多关键帧设置
- function updateBatchSegments() {
- const container = document.getElementById('batch-segments-container');
- if (!container) return;
-
- if (batchImages.length < 2) {
- container.innerHTML =
- '
' +
- escapeHtmlAttr(_t('batchNeedTwo')) +
- '
';
- return;
- }
-
- if (batchWorkflowIsSingle()) {
- if (batchImages.length >= 2) captureBatchKfTimelineFromDom();
- const n = batchImages.length;
- const defaultTotal = 8;
- const defaultSeg =
- n > 0 ? (defaultTotal / n).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 || '');
- const sdStored = batchKfSegDurByIndex[i];
- const segVal =
- sdStored !== undefined && sdStored !== ''
- ? escapeHtmlAttr(sdStored)
- : defaultSeg;
- blocks += `
-
-
-

-
- ${escapeHtmlAttr(_t('batchKfTitle'))} ${i + 1} / ${n}
- —
-
-
-
-
-
-
-
`;
- });
- container.innerHTML = `
-
-
-
${escapeHtmlAttr(_t('batchKfPanelTitle'))}
-
- ${escapeHtmlAttr(_t('batchTotalDur'))} — ${escapeHtmlAttr(_t('batchTotalSec'))}
-
-
-
${escapeHtmlAttr(_t('batchPanelHint'))}
-
- ${blocks}
-
-
`;
- updateBatchKfTimelineDerivedUI();
- return;
- }
-
- let html =
- '' +
- escapeHtmlAttr(_t('batchSegTitle')) +
- '
';
-
- for (let i = 0; i < batchImages.length - 1; i++) {
- const segPh = escapeHtmlAttr(_t('batchSegPromptPh'));
- html += `
-
-
-
-

-
→
-

-
${escapeHtmlAttr(_t('batchSegClip'))} ${i + 1}
-
-
-
-
- ${escapeHtmlAttr(_t('batchSegSec'))}
-
-
-
-
-
-
-
- `;
- }
-
- container.innerHTML = html;
- }
-
- let _isGeneratingFlag = false;
- let queuePollInterval = null;
- const queueTaskTerminalSeen = new Set();
- let lastAutoDisplayedQueueTaskId = null;
- let queuePollInFlight = false;
- let statusPollInFlight = false;
- let currentPreviewAssetUrl = '';
- let currentPreviewAssetName = '';
- let currentPreviewReplayId = '';
-
- function queueStatusLabel(task) {
- if (!task) return _t('queueIdle');
- const mapping = {
- queued: _t('queueQueued'),
- running: _t('queueRunning'),
- complete: _t('queueComplete'),
- error: _t('queueError'),
- cancelled: _t('queueCancelled')
- };
- return mapping[task.status] || task.status || '未知';
- }
-
- function queueModeLabel(task) {
- const mode = task?.mode || 'task';
- const mapping = {
- video: _t('queueTaskTypeVideo'),
- motion: _t('queueTaskTypeMotion'),
- batch: _t('queueTaskTypeBatch'),
- image: _t('queueTaskTypeImage')
- };
- return mapping[mode] || mode;
- }
-
- function renderQueueStatus(data) {
- const summary = document.getElementById('queue-summary');
- const list = document.getElementById('queue-list');
- if (!summary || !list) return;
-
- const pending = Array.isArray(data?.pending) ? data.pending : [];
- const current = data?.current || null;
- summary.textContent = current
- ? _fmt('queueRunningSummary', { n: pending.length })
- : (pending.length ? _fmt('queueWaiting', { n: pending.length }) : _t('queueIdle'));
-
- const items = [];
- if (current) items.push({ ...current, _current: true });
- items.push(...pending.slice(0, 4));
-
- if (!items.length) {
- list.innerHTML = `${_t('queueNoTasks')}
`;
- return;
- }
-
- list.innerHTML = items.map(task => {
- const title = escapeHtmlAttr(task.label || queueModeLabel(task));
- const status = queueStatusLabel(task);
- const meta = task._current
- ? `${status} · ${task.progress || 0}%`
- : (task.position ? _fmt('queuePosition', { n: task.position }) : status);
- const error = task.error ? `${escapeHtmlAttr(task.error)}
` : '';
- const resultPath = task.result?.video_path || task.result?.image_paths?.[0] || '';
- const openBtn = resultPath
- ? ``
- : '';
- return `
-
-
-
-
${title}
-
${queueModeLabel(task)} · ${meta}
-
-
${status}
-
- ${error}
- ${openBtn}
-
- `;
- }).join('');
- }
-
- async function pollQueueStatus() {
- if (queuePollInFlight) return;
- queuePollInFlight = true;
- try {
- const res = await fetch(`${BASE}/api/queue/status`);
- const data = await res.json();
- renderQueueStatus(data);
-
- const tasks = [];
- if (data?.current) tasks.push(data.current);
- if (Array.isArray(data?.history)) tasks.push(...data.history);
- for (const task of tasks) {
- if (!task || !task.id || queueTaskTerminalSeen.has(task.id)) continue;
- if (task.status !== 'complete' && task.status !== 'error' && task.status !== 'cancelled') continue;
- queueTaskTerminalSeen.add(task.id);
-
- if (task.status === 'complete') {
- addLog(_fmt('queueDoneLog', { label: task.label || task.id }));
- setTimeout(() => {
- isLoadingHistory = false;
- if (typeof fetchHistory === 'function') fetchHistory(1);
- }, 300);
- } else if (task.status === 'error') {
- addLog(_fmt('queueFailLog', { label: task.label || task.id, error: task.error || 'unknown' }));
- } else if (task.status === 'cancelled') {
- addLog(_fmt('queueCancelLog', { label: task.label || task.id }));
- }
- }
-
- if (!data?.current && Array.isArray(data?.history)) {
- const latestComplete = data.history.find(task => task && task.status === 'complete');
- if (latestComplete && latestComplete.id !== lastAutoDisplayedQueueTaskId) {
- const rawPath = latestComplete.result?.image_paths?.[0] || latestComplete.result?.video_path;
- const outputType = latestComplete.result?.image_paths?.length ? 'image' : 'video';
- if (rawPath) {
- lastAutoDisplayedQueueTaskId = latestComplete.id;
- try { displayOutput(rawPath, outputType, { silentLog: true, replay: latestComplete }); } catch (_) {}
- }
- }
- }
- } catch (_) {}
- finally {
- queuePollInFlight = false;
- }
- }
-
- function startQueuePolling() {
- if (queuePollInterval) return;
- queuePollInterval = setInterval(pollQueueStatus, 3000);
- pollQueueStatus();
- }
-
- // 系统状态轮询
- async function checkStatus() {
- if (statusPollInFlight) return;
- statusPollInFlight = true;
- 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'); }
- finally { statusPollInFlight = false; }
- }
- setInterval(checkStatus, 3000);
- checkStatus();
- startQueuePolling();
- initDragAndDrop();
- listGpus();
-
- updateResPreview();
- updateBatchResPreview();
- updateImgResPreview();
- refreshPromptPlaceholder();
-
- window.onUiLanguageChanged = function () {
- updateResPreview();
- updateBatchResPreview();
- updateImgResPreview();
- refreshPromptPlaceholder();
- if (typeof currentMode !== 'undefined' && currentMode === 'batch') {
- updateBatchSegments();
- }
- if (typeof currentMode !== 'undefined' && currentMode === 'tts') {
- checkTtsStatus();
- }
- updateLoraDropdown();
- updateBatchLoraDropdown();
- pollQueueStatus();
- };
-
- 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;
- // auto apply immediately
- 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) {}
- }
-
- async function saveLoraDir() {
- const input = document.getElementById('lora-dir-input');
- const status = document.getElementById('lora-dir-status');
- if (!input || !status) return;
-
- const loraDir = input.value.trim();
- try {
- const res = await fetch(`${BASE}/api/lora-dir`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ loraDir: loraDir })
- });
- const data = await res.json();
- if (data && data.status === 'ok') {
- status.textContent = '✓ 已保存';
- status.style.color = '#4caf50';
- scanLoras();
- setTimeout(() => { status.textContent = ''; }, 3000);
- } else {
- status.textContent = '✗ 保存失败: ' + (data.message || JSON.stringify(data));
- status.style.color = '#f44336';
- }
- } catch (e) {
- status.textContent = '✗ 保存失败: ' + e.message;
- status.style.color = '#f44336';
- }
- }
-
- async function loadLoraDir() {
- try {
- const res = await fetch(`${BASE}/api/lora-dir`);
- const data = await res.json();
- if (data && document.getElementById('lora-dir-input')) {
- document.getElementById('lora-dir-input').value = data.loraDir || '';
- const hintEl = document.getElementById('lora-placement-hint');
- if (hintEl) {
- const tpl = _t('loraPlacementHintWithDir');
- const modelsDir = data.modelsDir || '';
- if (modelsDir) {
- hintEl.innerHTML = tpl.replace('{dir}', escapeHtmlAttr(modelsDir));
- } else {
- hintEl.innerHTML = _t('loraPlacementHint');
- }
- }
- }
- } catch (e) {}
- }
-
- function isSwitchableDistilledCheckpoint(model) {
- const name = String(model?.name || '').toLowerCase();
- if (!name.endsWith('.safetensors')) return false;
- if (!name.includes('ltx-2.3') || !name.includes('22b') || !name.includes('distilled')) return false;
- return !name.includes('dev')
- && !name.includes('ic-lora')
- && !name.includes('control')
- && !name.includes('upscaler')
- && !name.includes('vae');
- }
-
- function getSelectedModelCheckpointPath() {
- const select = document.getElementById('model-checkpoint-select');
- return (select?.value || localStorage.getItem(MODEL_CHECKPOINT_STORAGE_KEY) || '').trim();
- }
-
- window.saveSelectedModelCheckpoint = function() {
- const select = document.getElementById('model-checkpoint-select');
- const status = document.getElementById('model-checkpoint-status');
- const value = (select?.value || '').trim();
- if (value) {
- localStorage.setItem(MODEL_CHECKPOINT_STORAGE_KEY, value);
- } else {
- localStorage.removeItem(MODEL_CHECKPOINT_STORAGE_KEY);
- }
- if (status) {
- status.textContent = value ? _t('modelCheckpointSaved') : _t('modelCheckpointHint');
- status.style.color = value ? '#4caf50' : 'var(--text-dim)';
- if (value) setTimeout(() => {
- status.textContent = _t('modelCheckpointHint');
- status.style.color = 'var(--text-dim)';
- }, 2500);
- }
- };
-
- window.loadModelCheckpoints = async function() {
- const select = document.getElementById('model-checkpoint-select');
- const status = document.getElementById('model-checkpoint-status');
- if (!select) return;
- const saved = localStorage.getItem(MODEL_CHECKPOINT_STORAGE_KEY) || '';
- try {
- if (status) {
- status.textContent = '...';
- status.style.color = 'var(--text-dim)';
- }
- const res = await fetch(`${BASE}/api/models`);
- const data = await res.json();
- const models = (data.models || []).filter(isSwitchableDistilledCheckpoint);
- select.innerHTML = '';
- const defOpt = document.createElement('option');
- defOpt.value = '';
- defOpt.textContent = _t('modelCheckpointDefault');
- select.appendChild(defOpt);
- models.forEach((model) => {
- const opt = document.createElement('option');
- opt.value = model.path;
- opt.textContent = model.name;
- select.appendChild(opt);
- });
- if (saved && models.some((model) => model.path === saved)) {
- select.value = saved;
- } else if (saved) {
- localStorage.removeItem(MODEL_CHECKPOINT_STORAGE_KEY);
- select.value = '';
- }
- if (status) {
- status.textContent = models.length ? _t('modelCheckpointHint') : _t('modelCheckpointNone');
- status.style.color = 'var(--text-dim)';
- }
- } catch (e) {
- if (status) {
- status.textContent = `${_t('modelCheckpointLoadFail')}: ${e.message}`;
- status.style.color = '#f44336';
- }
- }
- };
-
- 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');
- const tabMotion = document.getElementById('tab-motion');
- if (tabMotion) tabMotion.classList.toggle('active', m === 'motion');
- const tabTts = document.getElementById('tab-tts');
- if (tabTts) tabTts.classList.toggle('active', m === 'tts');
-
- 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';
- const motionOpts = document.getElementById('motion-opts');
- if (motionOpts) motionOpts.style.display = m === 'motion' ? 'block' : 'none';
- const ttsOpts = document.getElementById('tts-opts');
- if (ttsOpts) ttsOpts.style.display = m === 'tts' ? 'block' : 'none';
-
- // 主按钮:TTS 模式下隐藏(TTS 有自己的生成按钮)
- const mainBtn = document.getElementById('mainBtn');
- if (mainBtn) mainBtn.closest('div').style.display = m === 'tts' ? 'none' : '';
-
- // 视觉提示词:TTS 模式下隐藏(因为 TTS 有自己的输入框)
- const pc = document.getElementById('prompt-container');
- if (pc) pc.style.display = m === 'tts' ? 'none' : '';
-
- // 移除 TTS 模式下的上方分割线
- const mainTabsSection = document.getElementById('main-tabs-section');
- if (mainTabsSection) {
- mainTabsSection.style.borderBottom = m === 'tts' ? 'none' : '';
- mainTabsSection.style.paddingBottom = m === 'tts' ? '0' : '';
- }
-
- if (m === 'batch') updateBatchSegments();
- if (m === 'tts') checkTtsStatus();
- refreshPromptPlaceholder();
- }
-
- window.setVideoTransferMode = function(mode) {
- window._videoTransferMode = mode || 'action';
- const select = document.getElementById('motion-conditioning-type');
- const actionBtn = document.getElementById('motion-mode-action');
- const cameraBtn = document.getElementById('motion-mode-camera');
- const repaintBtn = document.getElementById('motion-mode-repaint');
- const actionControl = document.getElementById('motion-action-control-section');
- const imageSection = document.getElementById('motion-image-section');
- const strength = document.getElementById('motion-strength');
- const strengthVal = document.getElementById('motion-strength-val');
- const strengthLabel = document.getElementById('motion-strength-label');
- const isAction = window._videoTransferMode === 'action';
- const isRepaint = window._videoTransferMode === 'repaint';
- if (select) select.value = isAction ? 'pose' : 'video';
- if (actionControl) actionControl.style.display = isAction ? 'block' : 'none';
- if (imageSection) imageSection.style.display = isRepaint ? 'none' : 'block';
- if (strength) {
- strength.min = '0';
- strength.max = '2';
- strength.step = '0.05';
- strength.value = '1';
- if (strengthVal) strengthVal.textContent = strength.value;
- }
- if (strengthLabel) {
- strengthLabel.textContent = isRepaint ? '重绘幅度' : _t('motionControlStrength');
- }
-
- const activeStyle = (btn, active) => {
- if (!btn) return;
- btn.style.background = active ? 'var(--accent)' : 'var(--panel-2)';
- btn.style.color = active ? '#fff' : 'var(--text-sub)';
- btn.style.borderColor = active ? 'var(--accent)' : 'var(--border)';
- };
- activeStyle(actionBtn, isAction);
- activeStyle(cameraBtn, window._videoTransferMode === 'camera');
- activeStyle(repaintBtn, isRepaint);
- };
-
- function refreshPromptPlaceholder() {
- const pe = document.getElementById('prompt');
- if (!pe) return;
- pe.placeholder =
- currentMode === 'tts' ? '切换到 TTS 模式时此框不参与生成' :
- _t('promptPlaceholder');
- }
-
- // ─── TTS 语音合成 ──────────────────────────────────────────────────────────
- let _ttsRefB64 = null; // 存放参考音频的 base64 内容
-
- window.onTtsModeChange = function() {
- const mode = document.getElementById('tts-mode').value;
- const refSec = document.getElementById('tts-ref-section');
- const ultSec = document.getElementById('tts-ultimate-section');
- if (refSec) refSec.style.display = (mode === 'clone' || mode === 'ultimate_clone') ? 'block' : 'none';
- if (ultSec) ultSec.style.display = mode === 'ultimate_clone' ? 'block' : 'none';
- };
-
- window.handleTtsRefUpload = async function(file) {
- if (!file) return;
- const placeholder = document.getElementById('tts-ref-placeholder');
- const statusEl = document.getElementById('tts-ref-status');
- const clearBtn = document.getElementById('tts-ref-clear');
- addLog(`正在读取参考音频: ${file.name}...`);
- const reader = new FileReader();
- reader.onload = async (e) => {
- const b64 = e.target.result;
- _ttsRefB64 = b64.includes(',') ? b64.split(',')[1] : b64;
- if (placeholder) placeholder.style.display = 'none';
- if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = '✅ ' + file.name; }
- if (clearBtn) clearBtn.style.display = 'flex';
- addLog(`✅ 参考音频已加载: ${file.name}`);
- };
- reader.readAsDataURL(file);
- };
-
- window.clearTtsRef = function() {
- _ttsRefB64 = null;
- const fi = document.getElementById('tts-ref-input');
- if (fi) fi.value = '';
- const placeholder = document.getElementById('tts-ref-placeholder');
- const statusEl = document.getElementById('tts-ref-status');
- const clearBtn = document.getElementById('tts-ref-clear');
- if (placeholder) placeholder.style.display = 'block';
- if (statusEl) { statusEl.style.display = 'none'; statusEl.textContent = ''; }
- if (clearBtn) clearBtn.style.display = 'none';
- addLog('🧹 已清除参考音频');
- };
-
- async function checkTtsStatus() {
- const bar = document.getElementById('tts-status-bar');
- if (!bar) return;
- try {
- const res = await fetch(`${BASE}/api/tts/status`);
- const data = await res.json();
- if (data.available) {
- bar.style.color = 'var(--accent)';
- bar.style.borderColor = 'var(--accent)';
- bar.textContent = _t('ttsStatusReady') + data.model_dir;
- } else if (!data.voxcpm_installed) {
- bar.style.color = '#f87171';
- bar.textContent = _t('ttsStatusNoPkq');
- } else if (!data.model_dir_exists) {
- bar.style.color = '#f87171';
- bar.textContent = _t('ttsStatusNoDir') + (data.expected_model_dir || data.model_dir || 'models\\VoxCPM2');
- } else {
- bar.style.color = 'var(--text-dim)';
- bar.textContent = _t('ttsStatusNotAvail');
- }
- } catch (e) {
- bar.style.color = '#f87171';
- bar.textContent = _t('ttsStatusConnErr') + e.message;
- }
- }
-
- window.runTts = async function() {
- const text = (document.getElementById('tts-text')?.value || '').trim();
- if (!text) { addLog(_t('ttsErrNoText')); return; }
-
- const mode = document.getElementById('tts-mode')?.value || 'text_only';
- const cfg = parseFloat(document.getElementById('tts-cfg')?.value || 2.0);
- const steps = parseInt(document.getElementById('tts-steps')?.value || 10);
- const promptText = document.getElementById('tts-prompt-text')?.value || '';
-
- if ((mode === 'clone' || mode === 'ultimate_clone') && !_ttsRefB64) {
- addLog(_t('ttsErrNoRef'));
- return;
- }
-
- const btn = document.getElementById('tts-gen-btn');
- if (btn) { btn.disabled = true; btn.textContent = _t('ttsGenBusy'); }
- const resultSec = document.getElementById('tts-result-section');
- if (resultSec) resultSec.style.display = 'none';
-
- addLog(`🎙️ TTS: Mode=${mode}, Length=${text.length}`);
-
- try {
- const payload = {
- text,
- mode,
- cfg_value: cfg,
- inference_timesteps: steps,
- reference_wav: _ttsRefB64 || null,
- prompt_wav: (mode === 'ultimate_clone' && _ttsRefB64) ? _ttsRefB64 : null,
- prompt_text: promptText || null,
- };
-
- const res = await fetch(`${BASE}/api/tts/generate`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload)
- });
- const data = await res.json();
-
- if (!res.ok || data.status !== 'complete') {
- throw new Error(data.error || 'Generation failed');
- }
-
- const audioUrl = `${BASE}${data.audio_url}`;
- const player = document.getElementById('tts-audio-player');
- const dlLink = document.getElementById('tts-download-link');
-
- // 播放音频
- if (player) { player.src = audioUrl; player.load(); }
- // 直接下载音频,不跳转页面
- if (dlLink) {
- try {
- const resp = await fetch(audioUrl);
- const blob = await resp.blob();
- const blobUrl = URL.createObjectURL(blob);
- dlLink.href = blobUrl;
- dlLink.download = data.audio_path || 'tts_output.wav';
- // 不再自动触发点击下载
- // dlLink.click();
- } catch (e) {
- // 若下载失败,回退到直接链接方式
- dlLink.href = audioUrl;
- dlLink.download = data.audio_path || 'tts_output.wav';
- }
- }
- if (resultSec) resultSec.style.display = 'block';
-
- addLog(`✅ TTS: ${data.audio_path} (${data.sample_rate} Hz)`);
-
- // 刷新历史记录
- setTimeout(() => { isLoadingHistory = false; if (typeof fetchHistory === 'function') fetchHistory(1); }, 500);
-
- } catch (e) {
- addLog(`❌ TTS Error: ${e.message}`);
- } finally {
- if (btn) { btn.disabled = false; btn.textContent = _t('ttsGenBtn'); }
- }
- };
- // ──────────────────────────────────────────────────────────────────────────
-
- function showGeneratingView() {
- if (!_isGeneratingFlag) return;
- const resImg = document.getElementById('res-img');
- const videoWrapper = document.getElementById('video-wrapper');
- const audioWrapper = document.getElementById('audio-wrapper');
- if (resImg) resImg.style.display = "none";
- if (videoWrapper) videoWrapper.style.display = "none";
- if (audioWrapper) audioWrapper.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 audio = document.getElementById('res-audio');
- if (audioPlayer) { try { audioPlayer.stop(); } catch(_) {} }
- if (audio) { audio.pause(); audio.removeAttribute('src'); audio.load(); }
- const loadingTxt = document.getElementById('loading-txt');
- if (loadingTxt) loadingTxt.style.display = "flex";
- }
-
- function removeCurrentLoadingCard() {
- const loadingCard = document.getElementById('current-loading-card');
- if (loadingCard) loadingCard.remove();
- }
-
- async function submitQueuedTask(mode, endpoint, payload, label) {
- payload = ensureReplaySeed(payload);
- const res = await fetch(`${BASE}/api/queue/submit`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ mode, endpoint, payload, label })
- });
- const data = await res.json().catch(() => ({}));
- if (!res.ok) {
- const errMsg = data.error || data.detail || '队列提交失败';
- throw new Error(typeof errMsg === 'string' ? errMsg : JSON.stringify(errMsg));
- }
- startQueuePolling();
- return data;
- }
-
- function makeReplaySeed() {
- const arr = new Uint32Array(1);
- if (window.crypto && window.crypto.getRandomValues) {
- window.crypto.getRandomValues(arr);
- return (arr[0] % 2147483646) + 1;
- }
- return (Math.floor(Date.now() + Math.random() * 1000000) % 2147483646) + 1;
- }
-
- function refreshRandomSeedValue() {
- const el = document.getElementById('seed-value');
- if (el) el.value = String(makeReplaySeed());
- }
-
- function ensureReplaySeed(payload) {
- if (!payload || typeof payload !== 'object') return payload;
- const n = Number(payload.seed);
- if (!Number.isInteger(n) || n <= 0) {
- payload.seed = getActiveSeedForSubmit();
- }
- return payload;
- }
-
- function getSeedMode() {
- return document.querySelector('input[name="seed-mode"]:checked')?.value || 'random';
- }
-
- function getFixedSeedValue() {
- const el = document.getElementById('seed-value');
- let seed = parseInt(el?.value || '42', 10);
- if (!Number.isInteger(seed) || seed <= 0) seed = 42;
- seed = Math.min(seed, 2147483647);
- if (el) el.value = String(seed);
- return seed;
- }
-
- function getActiveSeedForSubmit() {
- return getFixedSeedValue();
- }
-
- function setFixedSeed(seed) {
- let seedNum = parseInt(seed, 10);
- if (!Number.isInteger(seedNum) || seedNum <= 0) {
- addLog(_t('previewNoReplaySeed'));
- return false;
- }
- seedNum = Math.min(seedNum, 2147483647);
- const fixed = document.querySelector('input[name="seed-mode"][value="fixed"]');
- const input = document.getElementById('seed-value');
- if (fixed) fixed.checked = true;
- if (input) input.value = String(seedNum);
- updateSeedModeUI();
- return true;
- }
-
- window.updateSeedModeUI = function() {
- const input = document.getElementById('seed-value');
- const fixed = getSeedMode() === 'fixed';
- const shell = input ? input.closest('.seed-input-shell') : null;
- document.querySelectorAll('.seed-mode-option').forEach((label) => {
- const checked = !!label.querySelector('input')?.checked;
- label.classList.toggle('is-active', checked);
- });
- if (input) {
- if (!input.value || Number(input.value) <= 0) refreshRandomSeedValue();
- input.disabled = !fixed;
- input.title = fixed ? _t('seedFixed') : _t('seedRandom');
- }
- if (shell) shell.classList.toggle('is-random', !fixed);
- };
-
- const replayStore = new Map();
- let replayStoreSeq = 0;
- const pendingReplaySeedByMode = {};
-
- function makeReplayRecord(source) {
- const payload = source?.payload || {};
- const endpoint = source?.endpoint || (source?.mode === 'image' ? '/api/generate-image' : '/api/generate');
- let mode = source?.mode || 'video';
- if (endpoint === '/api/generate-image') mode = 'image';
- else if (endpoint === '/api/ic-lora/generate') mode = 'motion';
- else if (endpoint === '/api/generate-batch') mode = 'batch';
- return {
- mode,
- endpoint,
- payload: JSON.parse(JSON.stringify(payload)),
- label: source?.label || `${mode}: ${(payload.prompt || '').slice(0, 48) || 'replay'}`
- };
- }
-
- function storeReplayRecord(source) {
- const id = `replay-${++replayStoreSeq}`;
- replayStore.set(id, makeReplayRecord(source));
- return id;
- }
-
- async function replayRecordById(id) {
- const record = replayStore.get(id);
- if (!record || !record.endpoint || !record.payload) {
- addLog(_t('replayMissing'));
- return;
- }
- try {
- const info = await submitQueuedTask(
- record.mode,
- record.endpoint,
- record.payload,
- `${_t('replayLabel')}: ${record.label || record.mode}`
- );
- addLog(_fmt('replayQueuedLog', { id: info.task_id }));
- } catch (e) {
- addLog(`❌ ${_t('replayFailed')}: ${e.message || e}`);
- }
- }
-
- function pathFileName(path) {
- return String(path || '').split(/[\\/]/).pop() || '';
- }
-
- function mediaUrlForPath(path) {
- return `${BASE}/api/system/file?path=${encodeURIComponent(path)}`;
- }
-
- function setElValue(id, value) {
- const el = document.getElementById(id);
- if (el && value !== undefined && value !== null) el.value = value;
- }
-
- function setFramePath(frameType, path) {
- if (!path) return;
- setElValue(`${frameType}-frame-path`, path);
- const preview = document.getElementById(`${frameType}-frame-preview`);
- const placeholder = document.getElementById(`${frameType}-frame-placeholder`);
- const clearOverlay = document.getElementById(`clear-${frameType}-frame-overlay`);
- if (preview) {
- preview.src = mediaUrlForPath(path);
- preview.style.display = 'block';
- }
- if (placeholder) placeholder.style.display = 'none';
- if (clearOverlay) clearOverlay.style.display = 'flex';
- }
-
- function setNamedUpload(path, hiddenId, placeholderId, statusId, nameId, clearId) {
- if (!path) return;
- setElValue(hiddenId, path);
- const placeholder = document.getElementById(placeholderId);
- const status = document.getElementById(statusId);
- const name = document.getElementById(nameId);
- const clear = document.getElementById(clearId);
- if (placeholder) placeholder.style.display = 'none';
- if (status) status.style.display = 'block';
- if (name) name.textContent = pathFileName(path);
- if (clear) clear.style.display = 'flex';
- }
-
- function setMotionImagePath(path) {
- if (!path) return;
- setElValue('motion-image-path', path);
- const preview = document.getElementById('motion-image-preview');
- const placeholder = document.getElementById('motion-image-placeholder');
- const clear = document.getElementById('clear-motion-image-overlay');
- if (preview) {
- preview.src = mediaUrlForPath(path);
- preview.style.display = 'block';
- }
- if (placeholder) placeholder.style.display = 'none';
- if (clear) clear.style.display = 'flex';
- }
-
- function applyLoraReplay(containerId, paths, strengths) {
- const container = document.getElementById(containerId);
- if (!container) return;
- container.innerHTML = '';
- const list = Array.isArray(paths) && paths.length ? paths : [];
- if (!list.length) {
- window.addLoraSelection(containerId);
- return;
- }
- list.forEach((path, index) => {
- window.addLoraSelection(containerId);
- const entry = container.lastElementChild;
- if (!entry) return;
- const select = entry.querySelector('.lora-select');
- const strength = entry.querySelector('.lora-strength');
- const strengthVal = entry.querySelector('.lora-strength-val');
- const strengthBox = entry.querySelector('.lora-strength-container');
- if (select) {
- if (![...select.options].some(opt => opt.value === path)) {
- const opt = document.createElement('option');
- opt.value = path;
- opt.textContent = pathFileName(path);
- select.appendChild(opt);
- }
- select.value = path;
- }
- const s = Array.isArray(strengths) ? strengths[index] : 2.0;
- const safeStrength = Math.min(2.0, Math.max(0.1, Number(s || 2.0)));
- if (strength) strength.value = String(safeStrength);
- if (strengthVal) strengthVal.textContent = String(safeStrength);
- if (strengthBox) strengthBox.style.display = 'flex';
- });
- }
-
- function applyReplayPayloadById(id) {
- const record = replayStore.get(id);
- if (!record || !record.payload) {
- addLog(_t('replayMissing'));
- return;
- }
- const payload = record.payload;
- switchMode(record.mode);
- if (payload.seed) pendingReplaySeedByMode[record.mode] = payload.seed;
- if (payload.seed) setFixedSeed(payload.seed);
- setElValue('prompt', payload.prompt || '');
-
- if (record.mode === 'image') {
- setElValue('img-w', payload.width);
- setElValue('img-h', payload.height);
- setElValue('img-steps', payload.numSteps);
- const stepsVal = document.getElementById('stepsVal');
- if (stepsVal && payload.numSteps) stepsVal.innerText = payload.numSteps;
- updateImgResPreview();
- } else if (record.mode === 'video') {
- const q = String(payload.resolution || '').replace('p', '');
- if (q) setElValue('vid-quality', q);
- setElValue('vid-ratio', (payload.aspectRatio === 'ref' && payload.customWidth && payload.customHeight) ? 'custom' : payload.aspectRatio);
- setElValue('vid-custom-w', payload.customWidth);
- setElValue('vid-custom-h', payload.customHeight);
- setElValue('vid-duration', payload.duration);
- setElValue('vid-fps', payload.fps);
- setElValue('vid-motion', payload.cameraMotion);
- const audio = document.getElementById('vid-audio');
- if (audio && payload.audio !== undefined) audio.checked = String(payload.audio) === 'true';
- setFramePath('start', payload.startFramePath || payload.imagePath);
- setFramePath('end', payload.endFramePath);
- setNamedUpload(payload.audioPath, 'uploaded-audio-path', 'audio-upload-placeholder', 'audio-upload-status', 'audio-filename-status', 'clear-audio-overlay');
- applyLoraReplay('loras-container', payload.loraPaths || (payload.loraPath ? [payload.loraPath] : []), payload.loraStrengths || [payload.loraStrength]);
- updateResPreview();
- } else if (record.mode === 'motion') {
- const mode = payload.conditioning_type === 'video'
- ? ((Array.isArray(payload.images) && payload.images.length) ? 'camera' : 'repaint')
- : 'action';
- window.setVideoTransferMode(mode);
- setNamedUpload(payload.video_path, 'motion-video-path', 'motion-video-placeholder', 'motion-video-status', 'motion-video-name', 'clear-motion-video-overlay');
- const imgPath = Array.isArray(payload.images) && payload.images[0] ? payload.images[0].path : '';
- setMotionImagePath(imgPath);
- setElValue('motion-conditioning-type', payload.conditioning_type);
- setElValue('motion-fps', payload.fps);
- setElValue('motion-duration', payload.duration);
- setElValue('motion-strength', payload.conditioning_strength);
- const strengthVal = document.getElementById('motion-strength-val');
- if (strengthVal && payload.conditioning_strength !== undefined) strengthVal.textContent = String(payload.conditioning_strength);
- } else if (record.mode === 'batch') {
- const q = String(payload.resolution || '').replace('p', '');
- if (q) setElValue('batch-quality', q);
- setElValue('batch-ratio', (payload.aspectRatio === 'ref' && payload.customWidth && payload.customHeight) ? 'custom' : payload.aspectRatio);
- setElValue('batch-custom-w', payload.customWidth);
- setElValue('batch-custom-h', payload.customHeight);
- setElValue('vid-fps', payload.fps);
- setElValue('vid-motion', payload.cameraMotion);
- applyLoraReplay('batch-loras-container', payload.loraPaths || (payload.loraPath ? [payload.loraPath] : []), payload.loraStrengths || [payload.loraStrength]);
- const segments = Array.isArray(payload.segments) ? payload.segments : [];
- const keyframes = Array.isArray(payload.keyframePaths) ? payload.keyframePaths : [];
- if (keyframes.length) {
- const singleMode = document.querySelector('input[name="batch-workflow"][value="single"]');
- if (singleMode) singleMode.checked = true;
- batchImages = keyframes.map(path => ({ path, preview: mediaUrlForPath(path) }));
- renderBatchImages();
- updateBatchSegments();
- const strengths = Array.isArray(payload.keyframeStrengths) ? payload.keyframeStrengths : [];
- strengths.forEach((s, idx) => setElValue(`batch-kf-strength-${idx}`, s));
- const times = Array.isArray(payload.keyframeTimes) ? payload.keyframeTimes.map(Number) : [];
- if (times.length) {
- const total = Number(payload.duration || 0);
- for (let idx = 0; idx < keyframes.length; idx++) {
- const next = idx < times.length - 1 ? times[idx + 1] : total;
- const cur = times[idx] || 0;
- const dur = Math.max(0.1, (Number(next) || cur + 1) - cur);
- setElValue(`batch-kf-seg-dur-${idx}`, dur.toFixed(1));
- }
- updateBatchKfTimelineDerivedUI();
- }
- } else if (segments.length) {
- const segmentMode = document.querySelector('input[name="batch-workflow"][value="segments"]');
- if (segmentMode) segmentMode.checked = true;
- batchImages = [];
- segments.forEach((seg, idx) => {
- if (idx === 0 && seg.startImage) batchImages.push({ path: seg.startImage, preview: mediaUrlForPath(seg.startImage) });
- if (seg.endImage) batchImages.push({ path: seg.endImage, preview: mediaUrlForPath(seg.endImage) });
- });
- renderBatchImages();
- updateBatchSegments();
- segments.forEach((seg, idx) => {
- setElValue(`batch-segment-duration-${idx}`, seg.duration);
- setElValue(`batch-segment-prompt-${idx}`, seg.prompt);
- });
- }
- setElValue('batch-background-audio-path', payload.backgroundAudioPath);
- updateBatchResPreview();
- }
- addLog(_t('replayLoadedLog'));
- }
-
- window.loadCurrentPreviewSeed = function() {
- const record = currentPreviewReplayId ? replayStore.get(currentPreviewReplayId) : null;
- const seed = record?.payload?.seed;
- if (!seed) {
- addLog(_t('previewNoReplaySeed'));
- return;
- }
- if (setFixedSeed(seed)) {
- addLog(_fmt('previewSeedLoadedLog', { seed }));
- }
- };
-
- window.loadCurrentPreviewParams = function() {
- if (!currentPreviewReplayId || !replayStore.has(currentPreviewReplayId)) {
- addLog(_t('previewNoReplayParams'));
- return;
- }
- applyReplayPayloadById(currentPreviewReplayId);
- };
-
- async function refreshPreviewReplayFromFile(fileOrPath, fileName) {
- if (currentPreviewReplayId) return;
- try {
- const hasPath = String(fileOrPath || '').includes('\\') || String(fileOrPath || '').includes('/');
- const qs = hasPath
- ? `path=${encodeURIComponent(fileOrPath)}`
- : `filename=${encodeURIComponent(fileName || fileOrPath)}`;
- const res = await fetch(`${BASE}/api/system/replay?${qs}`);
- const data = await res.json().catch(() => ({}));
- if (!res.ok || !data.replay) return;
- currentPreviewReplayId = storeReplayRecord(data.replay);
- const actions = document.getElementById('preview-replay-actions');
- if (actions) {
- actions.style.display = 'inline-flex';
- actions.classList.remove('is-unavailable');
- }
- } catch (_) {}
- }
-
- window.replayRecordById = replayRecordById;
- window.applyReplayPayloadById = applyReplayPayloadById;
-
- async function run() {
- const useQueue = ['video', 'motion', 'batch', 'image'].includes(currentMode);
- // 防止重复点击(_isGeneratingFlag 比 btn.disabled 更可靠)
- 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;
- 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 === 'batch') {
- if (!batchHasUsablePrompt()) {
- addLog(_t('warnBatchPrompt'));
- return;
- }
- } else if (currentMode !== 'motion' && !prompt) {
- addLog(_t('warnNeedPrompt'));
- return;
- }
-
- if (!btn) {
- console.error('mainBtn not found');
- return;
- }
-
- // 先设置标志 + 禁用按钮,然后用顶层 try/finally 保证一定能解锁
- _isGeneratingFlag = true;
- btn.disabled = true;
-
- try {
- if (!useQueue) {
- 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 = `
-
- ${escapeHtmlAttr(_t('loaderGpuAlloc'))}
- `;
- }
- if (resImg) resImg.style.display = "none";
- const videoWrapper = document.getElementById('video-wrapper');
- if (videoWrapper) videoWrapper.style.display = "none";
- const audioWrapper = document.getElementById('audio-wrapper');
- if (audioWrapper) audioWrapper.style.display = "none";
- if (player) { try { player.stop(); } catch(_) {} }
- else if (resVideo) { resVideo.pause?.(); resVideo.removeAttribute?.('src'); }
- if (audioPlayer) { try { audioPlayer.stop(); } catch(_) {} }
-
- 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 = `
-
- 等待中...
- `;
- 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 genSize = getGenerationSize('video');
- 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;
- const modelPath = getSelectedModelCheckpointPath();
-
- let finalImagePath = null, finalStartFramePath = null, finalEndFramePath = null;
- if (startFramePathValue && endFramePathValue) {
- finalStartFramePath = startFramePathValue;
- finalEndFramePath = endFramePathValue;
- } else if (startFramePathValue) {
- finalImagePath = startFramePathValue;
- }
-
- endpoint = '/api/generate';
- const loraPaths = [];
- const loraStrengths = [];
- document.querySelectorAll('#loras-container .lora-entry').forEach(entry => {
- const sel = entry.querySelector('.lora-select');
- const strIn = entry.querySelector('.lora-strength');
- if (sel && sel.value) {
- loraPaths.push(sel.value);
- loraStrengths.push(strIn ? parseFloat(strIn.value) : 2.0);
- }
- });
-
- const effectiveLoraPaths = loraPaths;
- const effectiveLoraStrengths = loraStrengths;
- const loraPath = effectiveLoraPaths[0] || null;
- const loraStrength = effectiveLoraStrengths[0] || 2.0;
-
- 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,
- customWidth: genSize.width,
- customHeight: genSize.height,
- loraPath: loraPath,
- loraStrength: loraStrength,
- loraPaths: effectiveLoraPaths.length > 0 ? effectiveLoraPaths : null,
- loraStrengths: effectiveLoraStrengths.length > 0 ? effectiveLoraStrengths : null,
- modelPath: modelPath || null,
- };
-
- let loraLog = _t('loraNoneLabel');
- if (effectiveLoraPaths.length > 0) {
- loraLog = effectiveLoraPaths.map(p => p.split(/[/\\]/).pop()).join(', ');
- }
- addLog(`正在发起视频渲染: ${res}, 时长: ${dur}s, FPS: ${fps}, LoRA: ${loraLog}`);
-
- } else if (currentMode === 'motion') {
- const videoPath = document.getElementById('motion-video-path')?.value || '';
- const imagePath = document.getElementById('motion-image-path')?.value || '';
- if (!videoPath) {
- throw new Error(_t('motionErrNeedVideo'));
- }
- const transferMode = window._videoTransferMode || 'action';
- if (transferMode !== 'repaint' && !imagePath) {
- throw new Error(_t('motionErrNeedImage'));
- }
- const conditioningType = document.getElementById('motion-conditioning-type')?.value || 'canny';
- const motionFpsRaw = document.getElementById('motion-fps')?.value || '';
- const motionFps = motionFpsRaw ? parseInt(motionFpsRaw, 10) : null;
- const motionDurationRaw = document.getElementById('motion-duration')?.value || '';
- const motionDuration = motionDurationRaw ? parseFloat(motionDurationRaw) : null;
- let strength = parseFloat(document.getElementById('motion-strength')?.value || '0.5');
- if (!Number.isFinite(strength)) strength = 1.0;
- strength = Math.max(0, Math.min(2.0, strength));
- const motionPrompt = prompt || 'A high quality video of the target subject following the reference motion, coherent movement, stable identity, clean details';
- endpoint = '/api/ic-lora/generate';
- payload = {
- video_path: videoPath,
- conditioning_type: conditioningType,
- prompt: motionPrompt,
- conditioning_strength: strength,
- fps: motionFps,
- duration: motionDuration,
- num_inference_steps: 30,
- cfg_guidance_scale: 1.0,
- negative_prompt: "low quality, blurry, noisy, static noise, distorted",
- images: transferMode === 'repaint' ? [] : [{ path: imagePath, frame: 0, strength: 1.0 }]
- };
- if (!prompt) addLog(_t('motionDefaultPromptNotice'));
- addLog(`${_fmt('motionStartLog', { type: conditioningType, strength })}, ${_fmt('motionStartMeta', { fps: motionFps || 'auto', duration: motionDuration || 'auto' })}`);
-
- } else if (currentMode === 'batch') {
- const res = updateBatchResPreview();
- const batchGenSize = getGenerationSize('batch');
- const loraPaths = [];
- const loraStrengths = [];
- document.querySelectorAll('#batch-loras-container .lora-entry').forEach(entry => {
- const sel = entry.querySelector('.lora-select');
- const strIn = entry.querySelector('.lora-strength');
- if (sel && sel.value) {
- loraPaths.push(sel.value);
- loraStrengths.push(strIn ? parseFloat(strIn.value) : 2.0);
- }
- });
-
- const effectiveLoraPaths = loraPaths;
- const effectiveLoraStrengths = loraStrengths;
- const loraPath = effectiveLoraPaths[0] || null;
- const loraStrength = effectiveLoraStrengths[0] || 2.0;
- const modelPath = getSelectedModelCheckpointPath();
-
- if (batchImages.length < 2) {
- throw new Error(_t('errBatchMinImages'));
- }
-
- if (batchWorkflowIsSingle()) {
- captureBatchKfTimelineFromDom();
- const fps = document.getElementById('vid-fps').value;
- const nKf = batchImages.length;
- const minSeg = 0.1;
- const frameDurs = [];
- for (let j = 0; j < nKf; j++) {
- let v = parseFloat(document.getElementById(`batch-kf-seg-dur-${j}`)?.value);
- if (!Number.isFinite(v) || v < minSeg) v = minSeg;
- frameDurs.push(v);
- }
- const sumSec = frameDurs.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 += frameDurs[j];
- times.push(acc);
- }
- const combinedPrompt = prompt.trim();
- if (!combinedPrompt) {
- throw new Error(_t('errSingleKfPrompt'));
- }
- 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,
- customWidth: batchGenSize.width,
- customHeight: batchGenSize.height,
- loraPath: loraPath,
- loraStrength: loraStrength,
- loraPaths: effectiveLoraPaths.length > 0 ? effectiveLoraPaths : null,
- loraStrengths: effectiveLoraStrengths.length > 0 ? effectiveLoraStrengths : null,
- modelPath: modelPath || null,
- };
- 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(), 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,
- customWidth: batchGenSize.width,
- customHeight: batchGenSize.height,
- loraPath: loraPath,
- loraStrength: loraStrength,
- loraPaths: effectiveLoraPaths.length > 0 ? effectiveLoraPaths : null,
- loraStrengths: effectiveLoraStrengths.length > 0 ? effectiveLoraStrengths : null,
- negativePrompt: "low quality, blurry, noisy, static noise, distorted",
- backgroundAudioPath: backgroundAudioPath || null,
- modelPath: modelPath || null
- };
- addLog(`分段拼接: ${segments.length} 段, ${res}${backgroundAudioPath ? ',含统一配乐' : ''}`);
- }
- }
-
- if (useQueue) {
- if (pendingReplaySeedByMode[currentMode]) {
- payload.seed = Number(pendingReplaySeedByMode[currentMode]);
- delete pendingReplaySeedByMode[currentMode];
- }
- const queueInfo = await submitQueuedTask(
- currentMode,
- endpoint,
- payload,
- `${currentMode}: ${(payload.prompt || prompt || '').slice(0, 48) || 'untitled'}`
- );
- addLog(_fmt('queueSubmitLog', { id: queueInfo.task_id, n: Math.max(0, (queueInfo.position || 1) - 1) }));
- if (getSeedMode() === 'random') refreshRandomSeedValue();
- } else {
- payload = ensureReplaySeed(payload);
- 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);
- if (getSeedMode() === 'random') refreshRandomSeedValue();
- }
-
- } catch (e) {
- removeCurrentLoadingCard();
- 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;
- if (!useQueue) {
- removeCurrentLoadingCard();
- stopProgressPolling();
- }
- checkStatus();
- // 生成完毕后自动释放显存(不 await 避免阻塞 UI 解锁)
- if (!useQueue) {
- 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 =>
- ``
- ).join('');
-
- // 更新当前显示的 GPU 名称
- 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 getAudioMime(filename) {
- const ext = String(filename || '').split('.').pop().toLowerCase();
- const map = { wav: 'audio/wav', mp3: 'audio/mpeg', flac: 'audio/flac', ogg: 'audio/ogg', m4a: 'audio/mp4', aac: 'audio/aac' };
- return map[ext] || 'audio/wav';
- }
-
- window.toggleAudioPreviewPlayback = function() {
- const audio = document.getElementById('res-audio');
- if (audioPlayer) {
- if (audioPlayer.playing) {
- audioPlayer.pause();
- return;
- }
- const playPromise = audioPlayer.play();
- if (playPromise && typeof playPromise.catch === 'function') {
- playPromise.catch(() => {});
- }
- return;
- }
- if (!audio) return;
- if (audio.paused) {
- const playPromise = audio.play();
- if (playPromise && typeof playPromise.catch === 'function') {
- playPromise.catch(() => {});
- }
- } else {
- audio.pause();
- }
- };
-
- function initAudioPreviewToggle() {
- const art = document.getElementById('audio-preview-art');
- if (!art || art.dataset.toggleBound === '1') return;
- art.dataset.toggleBound = '1';
- art.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- window.toggleAudioPreviewPlayback();
- });
- art.addEventListener('keydown', (e) => {
- if (e.key !== 'Enter' && e.key !== ' ') return;
- e.preventDefault();
- window.toggleAudioPreviewPlayback();
- });
- }
-
- function displayOutput(fileOrPath, outputType = null, options = {}) {
- const img = document.getElementById('res-img');
- const vid = document.getElementById('res-video');
- const audio = document.getElementById('res-audio');
- const audioWrapper = document.getElementById('audio-wrapper');
- const audioTitle = document.getElementById('audio-preview-title');
- const loader = document.getElementById('loading-txt');
- const videoWrapper = document.getElementById('video-wrapper');
- const previewDownloadBtn = document.getElementById('preview-download-btn');
- const previewReplayActions = document.getElementById('preview-replay-actions');
- const effectiveType = outputType || currentMode;
- const silentLog = !!options.silentLog;
-
- currentPreviewAssetUrl = '';
- currentPreviewAssetName = '';
- currentPreviewReplayId = options.replay ? storeReplayRecord(options.replay) : (options.replayId || '');
- if (previewDownloadBtn) previewDownloadBtn.style.display = 'none';
- if (previewReplayActions) {
- previewReplayActions.style.display = 'none';
- previewReplayActions.classList.toggle('is-unavailable', !currentPreviewReplayId);
- }
-
- if (img) img.style.display = "none";
- if (videoWrapper) videoWrapper.style.display = "none";
- if (audioWrapper) audioWrapper.style.display = "none";
-
- // 关键BUG修复:切换前强制清除并停止现有视频和声音,避免后台继续播放
- if(player) {
- player.stop();
- } else {
- vid.pause();
- vid.removeAttribute('src');
- vid.load();
- }
- if (audio) {
- audio.pause();
- audio.removeAttribute('src');
- audio.load();
- }
- if (audioPlayer) {
- try { audioPlayer.stop(); } catch(_) {}
- }
-
- 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 (effectiveType === 'audio') {
- currentPreviewAssetUrl = url;
- currentPreviewAssetName = fileName || 'preview.wav';
- if (previewDownloadBtn) previewDownloadBtn.style.display = 'inline-flex';
- if (audioTitle) audioTitle.textContent = fileName;
- if (audioWrapper) audioWrapper.style.display = "flex";
- if (audioPlayer) {
- audioPlayer.source = {
- type: 'audio',
- sources: [{ src: url, type: getAudioMime(fileName) }]
- };
- const playPromise = audioPlayer.play();
- if (playPromise && typeof playPromise.catch === 'function') {
- playPromise.catch(() => {});
- }
- } else if (audio) {
- audio.src = url;
- audio.load();
- const playPromise = audio.play();
- if (playPromise && typeof playPromise.catch === 'function') {
- playPromise.catch(() => {});
- }
- }
- if (!silentLog) addLog(`✅ 音频加载成功: ${fileName}`);
- } else if (effectiveType === 'image') {
- currentPreviewAssetUrl = url;
- currentPreviewAssetName = fileName || 'preview.png';
- if (previewDownloadBtn) previewDownloadBtn.style.display = 'inline-flex';
- if (previewReplayActions) previewReplayActions.style.display = 'inline-flex';
- refreshPreviewReplayFromFile(fileOrPath, fileName);
- img.src = url;
- img.style.display = "block";
- if (!silentLog) addLog(`✅ 图像渲染成功: ${fileName}`);
- } else {
- if (videoWrapper) videoWrapper.style.display = "flex";
- currentPreviewAssetUrl = url;
- currentPreviewAssetName = fileName || 'preview.mp4';
- if (previewDownloadBtn) previewDownloadBtn.style.display = 'inline-flex';
- if (previewReplayActions) previewReplayActions.style.display = 'inline-flex';
- refreshPreviewReplayFromFile(fileOrPath, fileName);
-
- if(player) {
- player.source = {
- type: 'video',
- sources: [{ src: url, type: 'video/mp4' }]
- };
- player.play();
- } else {
- vid.src = url;
- }
- if (!silentLog) addLog(`✅ 视频渲染成功: ${fileName}`);
- }
- }
-
- window.downloadCurrentPreviewAsset = function() {
- if (!currentPreviewAssetUrl) {
- addLog(_t('previewNoDownload'));
- return;
- }
- const a = document.createElement('a');
- a.href = currentPreviewAssetUrl;
- a.download = currentPreviewAssetName || 'preview.bin';
- document.body.appendChild(a);
- a.click();
- a.remove();
- };
-
-
-
- function addLog(msg) {
- const log = document.getElementById('log');
- if (!log) {
- console.log('[LTX]', msg);
- return;
- }
- const time = new Date().toLocaleTimeString();
- log.innerHTML += ` [${time}] ${msg}
`;
- log.scrollTop = log.scrollHeight;
- }
-
-
-// Force switch to video mode on load
-window.addEventListener('DOMContentLoaded', () => switchMode('video'));
-window.addEventListener('DOMContentLoaded', () => {
- refreshRandomSeedValue();
- updateSeedModeUI();
-});
-
-
-
-
-
-
-
-
-
-
-
-
- let currentHistoryPage = 1;
- let isLoadingHistory = false;
- const HISTORY_PAGE_LIMIT = 240;
- /** 与上次成功渲染一致时,silent 轮询跳过整表 innerHTML,避免缩略图周期性重新加载 */
- 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=${HISTORY_PAGE_LIMIT}`);
- if (!res.ok) {
- isLoadingHistory = false;
- return;
- }
- const data = await res.json();
-
- const validHistory = (data.history || []).filter(item => {
- if (!item || !item.filename) return false;
- const name = String(item.filename);
- if (name.startsWith('_') || name.toLowerCase().startsWith('tmp')) return false;
- const size = Number(item.size || 0);
- if (item.type === 'video' && size > 0 && size < 4096) return false;
- return true;
- });
- 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'
- ? ``
- : item.type === 'audio'
- ? `♪
${escapeHtmlAttr(item.filename)}
`
- : `
`;
- const typeBadge = item.type === 'video' ? '🎬 VID' : item.type === 'audio' ? '♪ AUD' : '🎨 IMG';
- const replayId = item.replay_available && item.replay ? storeReplayRecord(item.replay) : '';
- return `
-
${typeBadge}
-
- ${media}
-
`;
- }).join('');
-
- container.insertAdjacentHTML('beforeend', cardsHtml);
-
- // 重新绑定loading card点击事件
- 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');
-
- // 每次只加载3个媒体元素(图片或视频)
- 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') {
- const revealVideoThumb = () => {
- media.pause();
- revealThumb();
- };
- media.addEventListener('loadedmetadata', () => {
- const duration = Number.isFinite(media.duration) ? media.duration : 0;
- const targetTime = Math.max(0.08, Math.min(1.2, duration > 0 ? duration * 0.18 : 0.5));
- try {
- media.currentTime = targetTime;
- } catch (_) {
- revealVideoThumb();
- }
- }, { once: true });
- media.addEventListener('seeked', revealVideoThumb, { once: true });
- media.addEventListener('loadeddata', () => {
- if (media.currentTime > 0.04) revealVideoThumb();
- }, { 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 = 'auto';
- media.load();
- } else if (media.complete && media.naturalWidth > 0) {
- revealThumb();
- }
-
- loadedCount++;
- }
- });
-
- // 继续检查直到没有更多媒体需要加载
- if (loadedCount > 0) {
- setTimeout(loadVisibleImages, 100);
- }
- }
-
- // 监听history-wrapper的滚动事件来懒加载
- 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, replayId = '') {
- document.getElementById('res-img').style.display = 'none';
- document.getElementById('video-wrapper').style.display = 'none';
- const audioWrapper = document.getElementById('audio-wrapper');
- if (audioWrapper) audioWrapper.style.display = 'none';
- displayOutput(file, type, { replayId });
- }
-
- window.addEventListener('DOMContentLoaded', () => {
- initAudioPreviewToggle();
- // Initialize Plyr Custom Video Component
- if(window.Plyr) {
- player = new Plyr('#res-video', {
- controls: [
- 'play-large', 'play', 'progress', 'current-time',
- 'mute', 'volume', 'fullscreen'
- ],
- settings: [],
- loop: { active: true },
- autoplay: true
- });
- audioPlayer = new Plyr('#res-audio', {
- controls: [
- 'play', 'progress', 'current-time',
- 'mute', 'volume'
- ],
- settings: []
- });
- }
-
- // Fetch current directory context to show in UI
- 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(() => {
- /* 先同步输出目录再拉历史,避免短时间内两次 fetchHistory 整表重绘导致缩略图闪两下 */
- switchLibTab('history');
- });
-
- // Load LoRA dir from settings
- loadLoraDir();
- loadModelCheckpoints();
-
- 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();
- });
-
-
-async function saveVramLimit() {
- const lim = document.getElementById("vram-limit-input").value;
- const status = document.getElementById("vram-limit-status");
- status.textContent = "保存中...";
- try {
- const res = await fetch(`${BASE}/api/vram-limit`, {
- method: "POST", headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ vramLimit: lim })
- });
- const d = await res.json();
- if (d.status === 'ok') {
- status.textContent = "保存成功";
- status.style.color = '#4caf50';
- } else throw new Error(d.message || "Unknown error");
- } catch (e) {
- status.textContent = e.message;
- status.style.color = '#f44336';
- }
-}
-async function fetchVramLimit() {
- try {
- const res = await fetch(`${BASE}/api/vram-limit`);
- const d = await res.json();
- const input = document.getElementById("vram-limit-input");
- if (input) {
- input.value = (d.vramLimit !== undefined && d.vramLimit !== null && d.vramLimit !== '') ? d.vramLimit : '0';
- }
- } catch (e) {}
-}
-try { fetchVramLimit(); } catch(e) {}
-