Spaces:
Running on Zero
Running on Zero
Download frontend_cache/app.js from waveshaker/universal-lora-studio: direct link, hf CLI and curl.
- Browser
- Download file 57.7 kB
-
https://huggingface.co/spaces/waveshaker/universal-lora-studio/resolve/main/frontend_cache/app.js
- Command line
-
hf download hf://spaces/waveshaker/universal-lora-studio/frontend_cache/app.js
-
curl -L -o app.js https://huggingface.co/spaces/waveshaker/universal-lora-studio/resolve/main/frontend_cache/app.js
57.7 kB
// ==========================================================================
// 📝 TODO (Architecture / Enhancements)
// ==========================================================================
// 1. [Prompt Presets] 实现基于 ID 的 Prompt 远程动态加载
// - 方案:将超长 Prompt 抽离为集中的 JSON 文件(如: prompt-assets/presets.json)。
// - 逻辑:图库 (Gallery) 跳转时仅传递 ?id=xxx,前端 (Studio) 接收到 ID 后,跨域 fetch 该 JSON 并自动填充 Prompt 输入框。
// - 优点:彻底解决 URL 长度限制,实现前后端数据彻底解耦,方便统一维护。
// commit@70f742a386b9cbbfd6df774b19d727485ef5446f
// ==========================================================================
// 🛡️ Dynamic API Base URL
// ==========================================================================
const hostname = window.location.hostname;
let API_BASE_URL = "";
// 1. 如果在 GUI 预览环境(哪怕带 .static 后缀)
if (hostname.includes("universal-lora-studio-gui")) {
API_BASE_URL = "https://waveshaker-universal-lora-studio-test.hf.space";
}
// 2. 如果在本地开发环境
else if (hostname === "localhost" || hostname === "127.0.0.1") {
API_BASE_URL = "https://waveshaker-universal-lora-studio-test.hf.space";
}
// 3. 如果在测试环境(test Space)
else if (hostname.includes("universal-lora-studio-test")) {
API_BASE_URL = ""; // 同源,走相对路径
}
// 4. 如果在线上环境(prod Space)
else if (hostname.includes("universal-lora-studio.hf.space") || hostname.includes("universal-lora-studio.static")) {
API_BASE_URL = ""; // 同源,走相对路径
}
// ==========================================================================
// 🔐 ZeroGPU Token Handshake
// ==========================================================================
const ZEROGPU_HEADERS_MSG = "supports-zerogpu-headers";
// 监听 HF 父窗口发来的握手信号
window.addEventListener("message", (event) => {
if (event.data === ZEROGPU_HEADERS_MSG) {
window.supports_zerogpu_headers = true;
}
});
// 向父窗口声明支持,触发后续的 token 索要流程
(function announceZeroGpuSupport() {
const hostname = window.location.hostname;
if (!hostname.endsWith(".hf.space") && !hostname.includes(".dev.")) {
return; // 非 HF 环境,不执行
}
const origin = hostname.includes(".dev.")
? `https://moon-${hostname.split(".")[1]}.dev.spaces.huggingface.tech`
: "https://huggingface.co";
try {
window.parent.postMessage(ZEROGPU_HEADERS_MSG, origin);
} catch (e) {
console.warn("ZeroGPU handshake failed:", e);
}
})();
// 向父窗口索要 x-ip-token(带 2 秒超时兜底)
async function requestZeroGpuToken() {
if (!window.supports_zerogpu_headers) return null;
const hostname = window.location.hostname;
if (!hostname.endsWith(".hf.space") && !hostname.includes(".dev.")) return null;
const origin = hostname.includes(".dev.")
? `https://moon-${hostname.split(".")[1]}.dev.spaces.huggingface.tech`
: "https://huggingface.co";
try {
return await new Promise((resolve) => {
const channel = new MessageChannel();
const timeout = setTimeout(() => {
channel.port1.close();
resolve(null);
}, 2000);
channel.port1.onmessage = ({ data }) => {
clearTimeout(timeout);
resolve(data || null);
};
window.parent.postMessage("zerogpu-headers", origin, [channel.port2]);
});
} catch {
return null;
}
}
// ==========================================================================
// 🛡️ API Layer
// ==========================================================================
const BackendAPI = {
// 1. 心跳检测
checkHealth: async function() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const res = await fetch(`${API_BASE_URL}/api/health`, {
signal: controller.signal,
cache: 'no-store'
});
clearTimeout(timeoutId);
if (res.ok) {
const data = await res.json();
return data.status === "ok";
}
return false;
} catch (error) {
return false;
}
},
// 2. 获取全局配置 (Sampler/Scheduler & Root Mapping)
getConfig: async function() {
try {
const res = await fetch(`${API_BASE_URL}/api/config`, {
method: 'GET',
cache: 'no-store'
});
if (!res.ok) throw new Error("HTTP error " + res.status);
return await res.json();
} catch (error) {
console.warn("⚠️ Failed to fetch config", error);
return null;
}
},
// 3. 获取 Base Model 列表
getModels: async function() {
const res = await fetch(`${API_BASE_URL}/api/models`, {
method: 'GET',
cache: 'no-store'
});
if (!res.ok) throw new Error("HTTP error " + res.status);
return await res.json();
},
// 4. 获取角色 LoRA 图库数据
getLoras: async function() {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
try {
const res = await fetch(`${API_BASE_URL}/api/loras`, {
method: 'GET',
cache: 'no-store',
signal: controller.signal
});
clearTimeout(timeoutId);
if (!res.ok) throw new Error("HTTP error " + res.status);
return await res.json();
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
},
// 5. 提交生成任务
submitGenerate: async function(payload) {
const headers = { 'Content-Type': 'application/json' };
const tokenInfo = await requestZeroGpuToken();
if (tokenInfo) {
if (typeof tokenInfo === 'object') {
Object.assign(headers, tokenInfo);
} else if (typeof tokenInfo === 'string') {
headers['x-ip-token'] = tokenInfo;
}
}
const res = await fetch(`${API_BASE_URL}/api/generate`, {
method: 'POST',
cache: 'no-store',
headers: headers,
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const data = await res.json();
if (data.status === "error") throw new Error(data.message);
return data.task_id;
},
// 6. 轮询任务状态
pollStatus: async function(taskId, callbacks, retries = 0) {
const MAX_RETRIES = 150;
try {
const res = await fetch(`${API_BASE_URL}/api/status/${taskId}`, {
cache: 'no-store'
});
const data = await res.json();
if (data.status === "processing" || data.status === "queued") {
if (callbacks.onProcessing) {
callbacks.onProcessing(data.message || "Processing...");
}
if (retries >= MAX_RETRIES) {
const timeoutMsg = `Timeout: Backend exceeded ${MAX_RETRIES * 500 / 1000}s. Please try again.`;
if (callbacks.onError) callbacks.onError(timeoutMsg);
return;
}
setTimeout(() => this.pollStatus(taskId, callbacks, retries + 1), 100);
}
else if (data.status === "success") {
// 如果后端返回的是绝对路径或 data: 开头,直接使用;否则拼接 API_BASE_URL
let finalImageUrl = data.image;
if (finalImageUrl && !finalImageUrl.startsWith('data:') && !finalImageUrl.startsWith('http')) {
finalImageUrl = `${API_BASE_URL}${finalImageUrl}`;
}
if (callbacks.onSuccess) callbacks.onSuccess(finalImageUrl, data.seed, data.metrics, data.filename);
}
else if (data.status === "error") {
if (callbacks.onError) callbacks.onError(data.message);
}
else if (data.status === "not_found") {
if (callbacks.onError) callbacks.onError("Task not found (backend may have restarted).");
}
} catch (error) {
if (callbacks.onError) callbacks.onError(error.message);
}
},
};
// ==========================================================================
// 🧠 Global State & Utilities
// ==========================================================================
let generateStartTime = 0;
let currentStageStartTime = 0;
let currentStageName = "";
let isInitializing = false;
let shouldAnimateNextRender = false;
let globalModelData = {};
let currentGalleryData = [];
let renderedCount = 0;
const BATCH_SIZE = 20;
let selectedLora = "";
let backendState = { isReady: false, isChecking: false, healthInterval: null };
const ANIM_CONFIG = {
startY: 24,
stiffness: 280,
damping: 0.7,
staggerDelay: 30,
};
function toUnicodeBold(str) {
return str.split('').map(char => {
const code = char.charCodeAt(0);
if (code >= 65 && code <= 90) return String.fromCodePoint(0x1D5D4 + (code - 65));
if (code >= 97 && code <= 122) return String.fromCodePoint(0x1D5EE + (code - 97));
if (code >= 48 && code <= 57) return String.fromCodePoint(0x1D7EC + (code - 48));
return char;
}).join('');
}
function bindSlider(sliderId, inputId) {
const slider = document.getElementById(sliderId);
const input = document.getElementById(inputId);
slider.addEventListener('input', (e) => { input.value = e.target.value; });
input.addEventListener('input', (e) => {
let val = parseFloat(e.target.value);
if(val < parseFloat(slider.min)) val = slider.min;
if(val > parseFloat(slider.max)) val = slider.max;
slider.value = val;
});
}
bindSlider('width-slider', 'width-input');
bindSlider('height-slider', 'height-input');
bindSlider('steps-slider', 'steps-input');
bindSlider('cfg-slider', 'cfg-input');
const RATIO_PRESETS = {
"1:1": { w: 1024, h: 1024 },
"3:4": { w: 888, h: 1184 },
"9:16": { w: 768, h: 1368 },
"4:3": { w: 1184, h: 888 },
"16:9": { w: 1368, h: 768 }
};
document.querySelectorAll('.ratio-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.ratio-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const ratio = btn.dataset.ratio;
const preset = RATIO_PRESETS[ratio];
if (preset) {
document.getElementById('width-slider').value = preset.w;
document.getElementById('width-input').value = preset.w;
document.getElementById('height-slider').value = preset.h;
document.getElementById('height-input').value = preset.h;
}
});
});
function clearActiveRatio() {
document.querySelectorAll('.ratio-btn').forEach(b => b.classList.remove('active'));
}
document.getElementById('width-slider').addEventListener('input', clearActiveRatio);
document.getElementById('width-input').addEventListener('input', clearActiveRatio);
document.getElementById('height-slider').addEventListener('input', clearActiveRatio);
document.getElementById('height-input').addEventListener('input', clearActiveRatio);
function setUIState(isGenerating) {
const btn = document.getElementById('generate-btn');
if (!backendState.isReady) {
btn.disabled = true;
btn.innerText = "⏳ Waiting for Backend...";
} else if (isGenerating) {
btn.disabled = true;
btn.innerText = "⏳ Generating...";
} else {
btn.disabled = false;
btn.innerText = "Generate";
}
}
// 获取 Checkbox 元素
const autoScrollToggle = document.getElementById('auto-scroll-toggle');
function smartScroll() {
// 只有用户勾选了“自动滚动”,才强制贴底
if (autoScrollToggle && autoScrollToggle.checked) {
const logContent = document.getElementById('log-content');
// 用 requestAnimationFrame 确保在 DOM 高度更新后再执行滚动
requestAnimationFrame(() => {
logContent.scrollTop = logContent.scrollHeight;
});
}
}
function addLog(message, type = "normal") {
const logContent = document.getElementById('log-content');
const newLine = document.createElement('div');
newLine.className = `log-line ${type === "normal" ? "" : type}`;
const time = new Date().toLocaleTimeString('en-US', { hour12: false });
newLine.innerText = `[${time}] ${message}`;
logContent.appendChild(newLine);
if (logContent.childElementCount > 100) {
logContent.firstElementChild.remove();
}
// 插入后调用智能滚动
smartScroll();
}
// ==========================================================================
// 💓 Health Check & Initialization
// ==========================================================================
async function performHealthCheck() {
if (backendState.isChecking) return;
backendState.isChecking = true;
const dot = document.getElementById('backend-status-dot');
const wrapper = document.getElementById('backend-status-wrapper');
if (!backendState.isReady) {
setUIState(false);
dot.className = "status-dot loading";
wrapper.title = "Connecting to backend...";
}
const isHealthy = await BackendAPI.checkHealth();
if (isHealthy) {
if (!backendState.isReady) {
backendState.isReady = true;
setUIState(false);
dot.className = "status-dot online";
wrapper.title = "Backend online, ready to generate!";
await loadInitialData();
}
} else {
backendState.isReady = false;
setUIState(false);
dot.className = "status-dot offline";
wrapper.title = "Backend offline, trying to reconnect...";
}
backendState.isChecking = false;
}
function startHealthPolling() {
if (backendState.healthInterval) return;
backendState.healthInterval = setInterval(async () => {
if (backendState.isReady) {
clearInterval(backendState.healthInterval);
backendState.healthInterval = null;
return;
}
await performHealthCheck();
}, 3000);
}
async function loadInitialData() {
if (isInitializing) return;
isInitializing = true;
console.log("🟢 [Init] 开始初始化...");
document.getElementById('unet-select').disabled = true;
const config = await BackendAPI.getConfig();
if (!config) console.error("❌ 获取配置失败,请检查后端 API 是否正常");
const finalSamplers = config?.samplers || ["euler", "euler_ancestral", "euler_a", "dpmpp_2m", "dpmpp_2m_sde", "dpmpp_sde", "heun", "lms", "lcm"];
const finalSchedulers = config?.schedulers || ["simple", "beta", "normal", "karras", "exponential", "sgm_uniform"];
const rootMapping = config?.root_mapping || {};
if (config && config.default_prompt) {
document.getElementById('prompt').value = config.default_prompt;
}
document.getElementById('sampler').innerHTML = finalSamplers.map(s => `<option value="${s}">${s}</option>`).join('');
document.getElementById('scheduler').innerHTML = finalSchedulers.map(s => `<option value="${s}">${s}</option>`).join('');
const selectEl = document.getElementById('unet-select');
try {
const data = await BackendAPI.getModels();
console.log("🟢 [Init] Models 数据已拉取");
if (data.models) {
globalModelData = data.models;
selectEl.innerHTML = "";
Object.keys(data.models).forEach(modelName => {
const opt = document.createElement("option");
opt.value = modelName;
if (modelName.includes(" | ")) {
const parts = modelName.split(" | ");
const enginePart = toUnicodeBold(parts[0]);
const modelPart = parts[1];
opt.textContent = `${enginePart} | ${modelPart}`;
} else {
opt.textContent = toUnicodeBold(modelName);
}
selectEl.appendChild(opt);
});
let modelParam = null;
let loraParam = null;
if (window.location.hash) {
const hashStr = window.location.hash.substring(1);
const hashParams = new URLSearchParams(hashStr);
modelParam = hashParams.get('model');
loraParam = hashParams.get('lora');
}
if (!modelParam) {
const urlParams = new URLSearchParams(window.location.search);
modelParam = urlParams.get('model');
loraParam = urlParams.get('lora');
}
let targetModel = Object.keys(data.models)[0];
if (modelParam) {
const targetEngine = rootMapping[modelParam] || modelParam;
console.log(`🔗 URL解析: ${modelParam} -> ${targetEngine}`);
for (const key of Object.keys(data.models)) {
const modelEngine = data.models[key].engine;
if (modelEngine === targetEngine) {
targetModel = key;
console.log(`✅ 成功匹配到模型: ${key} (Engine: ${modelEngine})`);
break;
}
}
}
console.log(`🟢 [Init] 准备选中模型: ${targetModel}`);
selectEl.value = targetModel;
window._currentSelectedModel = targetModel;
window._lastUsedArchitecture = globalModelData[targetModel]?.engine || "unknown";
const modelDefaults = globalModelData[targetModel]?.defaults || {};
if (globalModelData[targetModel]?.default_prompt !== undefined) {
document.getElementById('prompt').value = globalModelData[targetModel].default_prompt;
}
if (modelDefaults.cfg !== undefined) {
document.getElementById('cfg-slider').value = modelDefaults.cfg;
document.getElementById('cfg-input').value = Number(modelDefaults.cfg).toFixed(1);
}
if (modelDefaults.steps !== undefined) {
document.getElementById('steps-slider').value = modelDefaults.steps;
document.getElementById('steps-input').value = modelDefaults.steps;
}
if (modelDefaults.sampler !== undefined) document.getElementById('sampler').value = modelDefaults.sampler;
if (modelDefaults.scheduler !== undefined) document.getElementById('scheduler').value = modelDefaults.scheduler;
if (modelDefaults.char_lora_strength !== undefined) {
document.getElementById('char-strength').value = Number(modelDefaults.char_lora_strength).toFixed(1);
}
selectedLora = "";
document.getElementById('selected-lora-name').innerText = "None";
document.getElementById('selected-lora-name').style.color = 'var(--text-muted)';
const bigImg = document.getElementById('selected-lora-big-img');
const placeholder = document.getElementById('preview-placeholder');
bigImg.src = "";
bigImg.style.display = 'none';
placeholder.style.display = 'flex';
document.querySelectorAll('.lora-card').forEach(c => c.classList.remove('selected'));
renderBaseLoras(targetModel);
window._isDataReady = true;
// 🌟 核心修改:如果 URL 没有指定 lora 参数,说明是普通进入,开启动画
const isPlainEntry = !loraParam;
await fetchAndRenderGallery(targetModel, isPlainEntry);
if (loraParam) {
if (!loraParam.endsWith(".safetensors")) {
console.warn(`⚠️ [Init] 忽略无效的 LoRA 参数(缺少 .safetensors 后缀): ${loraParam}`);
} else {
const targetIndex = currentGalleryData.findIndex(item => item.filename === loraParam);
if (targetIndex !== -1) {
const targetItem = currentGalleryData[targetIndex];
console.log(`🟢 [Init] 数据层成功找到 LoRA: ${loraParam},位于第 ${targetIndex + 1} 个`);
selectedLora = targetItem.filename;
const nameEl = document.getElementById('selected-lora-name');
nameEl.innerText = targetItem.title;
nameEl.style.color = 'var(--accent)';
const bigImg = document.getElementById('selected-lora-big-img');
const placeholder = document.getElementById('preview-placeholder');
const previewWrapper = document.querySelector('.preview-wrapper');
if (previewWrapper) previewWrapper.classList.add('shimmer-container');
bigImg.src = targetItem.webp;
bigImg.onload = () => {
bigImg.style.display = 'block';
placeholder.style.display = 'none';
if (previewWrapper) previewWrapper.classList.remove('shimmer-container');
bigImg.onload = null;
};
bigImg.onerror = () => {
if (previewWrapper) previewWrapper.classList.remove('shimmer-container');
bigImg.onerror = null;
};
if (targetIndex >= renderedCount) {
const requiredRenderCount = Math.ceil((targetIndex + 1) / BATCH_SIZE) * BATCH_SIZE;
const tempData = currentGalleryData.slice(renderedCount, requiredRenderCount);
const grid = document.getElementById('gallery-grid');
tempData.forEach(item => {
const card = document.createElement('div');
card.className = 'lora-card';
card.dataset.filename = item.filename;
card.innerHTML = `
<img src="${item.webp}" loading="lazy" alt="${item.title}" onerror="this.src='https://via.placeholder.com/300x400?text=No+Image'">
<div class="card-info">
<div class="card-title">${item.title}</div>
</div>
`;
const cardImg = card.querySelector('img');
applyCardShimmer(card, cardImg);
bindCardClick(card, item);
grid.appendChild(card);
});
renderedCount = requiredRenderCount;
}
setTimeout(() => {
const targetCard = document.querySelector(`.lora-card[data-filename="${loraParam}"]`);
if (targetCard) {
console.log(`🟢 [Init] DOM 已渲染,执行滚动和高亮`);
document.querySelectorAll('.lora-card').forEach(c => c.classList.remove('selected'));
targetCard.classList.add('selected');
targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {
console.warn(`⚠️ 极度异常:数据存在,但强制渲染后依然找不到 DOM`);
}
}, 150);
} else {
console.warn(`⚠️ [Init] 在返回的数据中不存在该 LoRA: ${loraParam}`);
}
}
}
}
} catch (e) {
console.error("❌ 初始化加载失败:", e);
selectEl.innerHTML = `<option value="">🔴 Backend Offline</option>`;
} finally {
document.getElementById('unet-select').disabled = false;
// 🌟 智能移除遮罩:如果数据没就绪,就等它就绪
const waitForDataAndHideMask = setInterval(() => {
if (window._isDataReady) {
clearInterval(waitForDataAndHideMask);
const mask = document.getElementById('global-loading-mask');
if (mask) {
setTimeout(() => {
mask.style.opacity = '0';
setTimeout(() => mask.remove(), 300);
}, 100); // 给 DOM 留 100ms 的渲染时间
}
}
}, 100);
}
}
// ==========================================================================
// 🖼️ Gallery & Card Rendering
// ==========================================================================
function renderBaseLoras(selectedModel) {
const container = document.getElementById('base-lora-container');
container.innerHTML = "";
const loras = globalModelData[selectedModel]?.base_loras || [];
if (loras.length === 0) {
container.innerHTML = `<div style="font-size: 12px; color: var(--text-muted);">No Base LoRA for current model</div>`;
return;
}
loras.forEach((lora) => {
const div = document.createElement('div');
div.className = 'lora-row';
div.innerHTML = `
<label>
<input type="checkbox" class="base-lora-cb" ${lora.defaultEnable ? 'checked' : ''}>
${lora.name}
</label>
<input type="number" class="strength-box base-lora-strength" value="1.0" step="0.05" title="Strength">
`;
container.appendChild(div);
});
}
// 在全局变量区加一个请求锁
let isGalleryFetching = false;
async function fetchAndRenderGallery(modelName, triggerAnim = false) {
// 🌟 核心修复 2:如果已经在请求中,直接锁死,不准并发
if (isGalleryFetching) {
console.log("⚠️ 拦截并发请求,当前图库正在加载中...");
return;
}
isGalleryFetching = true;
const grid = document.getElementById('gallery-grid');
const countEl = document.getElementById('lora-count');
// 🌟 注意:这里不再清空 grid.innerHTML,因为我们在 unet-select 里已经清空了
// 但为了首次加载的安全,如果 grid 里没有 Loading 提示,还是补一个
if (!grid.innerHTML.includes("Loading") && !grid.innerHTML.includes("Switching")) {
grid.innerHTML = `<div style="color: var(--text-muted); font-size: 13px; grid-column: span 4;">⏳ Loading character list...</div>`;
}
try {
const modelInfo = globalModelData[modelName];
if (!modelInfo || !modelInfo.engine) {
grid.innerHTML = `<div style="color: var(--text-muted); font-size: 13px; grid-column: span 4;">⏳ Backend data syncing...</div>`;
return;
}
const engineKey = modelInfo.engine;
// 🌟 别忘了你之前给 getLoras 加了 10 秒超时
const data = await BackendAPI.getLoras();
const lorasDict = data.loras || {};
const lorasForModel = lorasDict[engineKey];
if (!lorasForModel || Object.keys(lorasForModel).length === 0) {
grid.innerHTML = `<div style="color: var(--text-muted); font-size: 13px; grid-column: span 4;">No character LoRA for this model</div>`;
if (countEl) countEl.innerText = "0 Models";
return;
}
currentGalleryData = Object.entries(lorasForModel).map(([filename, info]) => ({
filename: filename,
title: filename.replace(".safetensors", ""),
webp: info.webp
}));
renderedCount = 0;
shouldAnimateNextRender = triggerAnim;
if (countEl) countEl.innerText = `${currentGalleryData.length} Models`;
// 🌟 核心:把 flex 居中还原成原来的网格布局
grid.style.display = 'grid';
grid.style.height = 'auto';
grid.innerHTML = "";
loadMoreCards();
const container = document.getElementById('gallery-container');
container.onscroll = () => {
if (container.scrollTop + container.clientHeight >= container.scrollHeight - 50) {
loadMoreCards();
}
};
} catch (error) {
console.error("Severe error in gallery request:", error);
grid.innerHTML = `<div style="color: #ff4444; font-size: 13px; grid-column: span 4;">❌ Failed to load gallery, backend may be restarting</div>`;
if (countEl) countEl.innerText = "Error";
} finally {
// 🌟 核心修复 3:无论成功失败,必须释放锁
isGalleryFetching = false;
}
}
function applyCardShimmer(card, imgElement) {
const overlay = document.createElement('div');
overlay.className = 'shimmer-overlay';
card.appendChild(overlay);
const removeShimmer = () => overlay.remove();
if (imgElement.complete) {
setTimeout(removeShimmer, 400);
} else {
imgElement.addEventListener('load', removeShimmer);
imgElement.addEventListener('error', removeShimmer);
}
}
function bindCardClick(card, item) {
card.addEventListener('click', () => {
document.querySelectorAll('.lora-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedLora = item.filename;
const nameEl = document.getElementById('selected-lora-name');
nameEl.innerText = item.title;
nameEl.style.color = item.title ? 'var(--accent)' : 'var(--text-muted)';
const bigImg = document.getElementById('selected-lora-big-img');
const placeholder = document.getElementById('preview-placeholder');
bigImg.style.display = 'none';
placeholder.style.display = 'flex';
bigImg.classList.add('shimmer-active');
bigImg.src = item.webp;
bigImg.onload = () => {
bigImg.style.display = 'block';
placeholder.style.display = 'none';
bigImg.onload = null;
};
bigImg.onerror = () => {
bigImg.style.display = 'none';
placeholder.style.display = 'flex';
bigImg.onerror = null;
};
setTimeout(() => {
bigImg.classList.remove('shimmer-active');
}, 400);
});
}
function loadMoreCards() {
const grid = document.getElementById('gallery-grid');
const nextBatch = currentGalleryData.slice(renderedCount, renderedCount + BATCH_SIZE);
if (nextBatch.length === 0) return;
// 🌟 注意这里要加 (item, index)
nextBatch.forEach((item, index) => {
const card = document.createElement('div');
card.className = 'lora-card';
card.dataset.filename = item.filename;
// 🌟 动画初始状态
if (shouldAnimateNextRender) {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
card.dataset.animateEntry = "true";
card.dataset.index = index;
}
card.innerHTML = `
<img src="${item.webp}" loading="lazy" alt="${item.title}" onerror="this.src='https://via.placeholder.com/300x400?text=No+Image'">
<div class="card-info">
<div class="card-title">${item.title}</div>
</div>
`;
const cardImg = card.querySelector('img');
applyCardShimmer(card, cardImg);
bindCardClick(card, item);
grid.appendChild(card);
// 🌟 动画执行
if (shouldAnimateNextRender) {
// 错开延迟触发,利用你配置里的 staggerDelay (30ms)
setTimeout(() => {
applySpringAnimation(card);
}, index * ANIM_CONFIG.staggerDelay);
}
});
renderedCount += nextBatch.length;
// 🌟 重要:动画只管第一屏。渲染完之后立刻关掉,防止滚动加载时还会闪烁。
shouldAnimateNextRender = false;
}
// ==========================================
// 🪄 Animation: 物理弹簧动画
// ==========================================
function applySpringAnimation(element) {
if (!element || element.dataset.animating === 'true') return;
element.dataset.animating = 'true';
const startY = ANIM_CONFIG.startY;
const omega0 = Math.sqrt(ANIM_CONFIG.stiffness);
const zeta = ANIM_CONFIG.damping;
const omegaD = omega0 * Math.sqrt(1 - zeta * zeta);
const startTime = performance.now();
element.style.transition = '';
element.style.opacity = '0';
element.style.transform = `translate3d(0, ${startY}px, 0)`;
element.style.willChange = 'transform, opacity';
element.style.backfaceVisibility = 'hidden';
void element.offsetWidth;
function step(currentTime) {
const t = (currentTime - startTime) / 1000;
const envelope = Math.exp(-zeta * omega0 * t);
if (Math.abs(envelope * startY) < 0.1) {
// 🌟 核心修复:不要用 translate3d,直接清空 transform,让父级彻底“干干净净”
element.style.transform = '';
element.style.opacity = 1;
element.style.willChange = 'auto';
element.style.transition = '';
element.dataset.animating = 'false';
return;
}
const positionY = startY * envelope * Math.cos(omegaD * t);
const opacity = Math.min(t / 0.25, 1);
element.style.transform = `translate3d(0, ${positionY}px, 0)`;
element.style.opacity = opacity;
requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
// ==========================================================================
// ⚡ Event Listeners (Generate, Switch, Random Seed)
// ==========================================================================
document.getElementById('random-seed-btn').addEventListener('click', (e) => {
e.preventDefault();
document.getElementById('seed').value = Math.floor(Math.random() * 4294967295);
});
document.getElementById('unet-select').addEventListener('change', async (e) => {
const selectedModel = e.target.value;
const currentEngine = globalModelData[selectedModel]?.engine || "unknown";
const lastEngine = window._lastUsedArchitecture || null;
const isCrossArchitecture = (lastEngine !== null && lastEngine !== currentEngine);
// 🌟 核心修复 1:如果是跨架构切换,第一时间强杀旧 DOM,别等网络请求
if (isCrossArchitecture) {
const grid = document.getElementById('gallery-grid');
// 🌟 核心:强行把 grid 改成 flex 居中
grid.style.display = 'flex';
grid.style.justifyContent = 'center';
grid.style.alignItems = 'center';
grid.style.height = '100%'; // 撑满父容器高度
grid.innerHTML = `<div style="color: var(--text-muted); font-size: 13px;">⏳ Loading LoRAs...</div>`;
document.getElementById('lora-count').innerText = "Loading...";
currentGalleryData = [];
renderedCount = 0;
}
// 1. 处理 Prompt 覆盖
const promptEl = document.getElementById('prompt');
const lastModelName = window._currentSelectedModel || null;
const previousDefaultPrompt = lastModelName ? (globalModelData[lastModelName]?.default_prompt || "") : "";
if (!promptEl.value.trim() || promptEl.value === previousDefaultPrompt) {
if (globalModelData[selectedModel]?.default_prompt) {
promptEl.value = globalModelData[selectedModel].default_prompt;
}
}
// 2. 处理参数更新 (不管跨不跨架构,参数肯定要更新)
const modelDefaults = globalModelData[selectedModel]?.defaults || {};
if (modelDefaults.cfg !== undefined) {
document.getElementById('cfg-slider').value = modelDefaults.cfg;
document.getElementById('cfg-input').value = Number(modelDefaults.cfg).toFixed(1);
}
if (modelDefaults.steps !== undefined) {
document.getElementById('steps-slider').value = modelDefaults.steps;
document.getElementById('steps-input').value = modelDefaults.steps;
}
if (modelDefaults.sampler !== undefined) {
document.getElementById('sampler').value = modelDefaults.sampler;
}
if (modelDefaults.scheduler !== undefined) {
document.getElementById('scheduler').value = modelDefaults.scheduler;
}
if (modelDefaults.char_lora_strength !== undefined && (isCrossArchitecture || !selectedLora)) {
document.getElementById('char-strength').value = Number(modelDefaults.char_lora_strength).toFixed(1);
}
// ========================================================
// 🌟 核心分支:只有跨架构才执行渲染,同架构直接躺平
// ========================================================
if (isCrossArchitecture) {
// 跨架构:清空已选角色,换 Base LoRA,重新拉数据(并传 true 触发入场动画)
selectedLora = "";
document.getElementById('selected-lora-name').innerText = "None";
document.getElementById('selected-lora-name').style.color = 'var(--text-muted)';
const bigImg = document.getElementById('selected-lora-big-img');
const placeholder = document.getElementById('preview-placeholder');
bigImg.src = "";
bigImg.style.display = 'none';
placeholder.style.display = 'flex';
document.querySelectorAll('.lora-card').forEach(c => c.classList.remove('selected'));
renderBaseLoras(selectedModel);
fetchAndRenderGallery(selectedModel, true); // 👈 后台异步加载,不阻塞事件
// 强制滚动到图库顶部
const galleryContainer = document.getElementById('gallery-container');
if (galleryContainer) galleryContainer.scrollTop = 0;
} else {
// 🌟 同架构:不请求网络,不动画廊,完美保留 DOM 和滚动位置
console.log("🔄 Same architecture, keeping gallery and Base LoRAs intact.");
}
// 更新全局记录
window._currentSelectedModel = selectedModel;
window._lastUsedArchitecture = currentEngine;
});
document.getElementById('generate-btn').addEventListener('click', async () => {
if (!backendState.isReady) {
alert("Backend is waking up. Please wait a few seconds and try again!");
return;
}
const resultImage = document.getElementById('result-image');
const placeholderText = document.getElementById('placeholder-text');
const generateBtn = document.getElementById('generate-btn');
const prompt = document.getElementById('prompt').value.trim();
const unet_choice = document.getElementById('unet-select').value;
const custom_w = parseInt(document.getElementById('width-input').value);
const custom_h = parseInt(document.getElementById('height-input').value);
const steps = parseInt(document.getElementById('steps-input').value);
const cfg = parseFloat(document.getElementById('cfg-input').value);
const sampler = document.getElementById('sampler').value;
const scheduler = document.getElementById('scheduler').value;
const seedVal = document.getElementById('seed').value;
const seed = seedVal === "" ? -1 : parseInt(seedVal);
if (!unet_choice || unet_choice.includes('Loading') || unet_choice.includes('Backend Offline')) {
alert("Please select a valid Base Model first."); return;
}
if (!prompt) {
alert("Please enter a prompt.");
return;
}
const baseLoraStates = [];
document.querySelectorAll('#base-lora-container .lora-row').forEach(row => {
const isChecked = row.querySelector('.base-lora-cb').checked;
const strength = parseFloat(row.querySelector('.base-lora-strength').value) || 1.0;
baseLoraStates.push(isChecked, strength);
});
const charEnable = document.getElementById('char-enable').checked;
const charStrength = parseFloat(document.getElementById('char-strength').value) || 1.0;
const requestData = {
prompt: prompt,
unet_choice: unet_choice,
custom_w: custom_w,
custom_h: custom_h,
steps: steps,
cfg: cfg,
seed: seed,
sampler: sampler,
scheduler: scheduler,
char_enable: charEnable,
char_lora: selectedLora || "",
char_strength: charStrength,
base_lora_states: baseLoraStates
};
console.log("🚀 Submitting parameters:", requestData);
if (!requestData.prompt || !requestData.unet_choice || !requestData.custom_w) {
alert("Missing parameters. Please check Prompt / Model / Dimensions.");
return;
}
generateBtn.disabled = true;
generateBtn.innerText = "⏳ Submitting...";
resultImage.style.display = 'none';
placeholderText.style.display = 'block';
generateStartTime = Date.now();
currentStageStartTime = generateStartTime;
currentStageName = "Task Queued";
document.getElementById('log-content').innerHTML = "";
if (window._lastGeneratedModel !== unet_choice) {
const currentEngine = globalModelData[unet_choice]?.engine || "unknown";
const lastModel = window._lastGeneratedModel;
const lastEngine = lastModel ? (globalModelData[lastModel]?.engine || "unknown") : null;
if (lastEngine && lastEngine !== currentEngine) {
addLog(`🔄 Arch Switch: Loading UNet/VAE/TE (Full Swap)...`, "highlight");
} else if (lastEngine === currentEngine) {
addLog(`🔄 Model Update: Swapping UNet (Keep VAE/TE)...`, "highlight");
} else {
addLog(`🔄 Initializing: Loading UNet/VAE/TE (First Load)...`, "highlight");
}
window._lastGeneratedModel = unet_choice;
}
addLog("Task submitted to backend.", "highlight");
try {
const taskId = await BackendAPI.submitGenerate(requestData);
addLog(`Task ID: ${taskId}`);
BackendAPI.pollStatus(taskId, {
onProcessing: (msg) => {
let safeMsg = msg || "🎨 Processing...";
let newStage = "Processing";
if (safeMsg.includes("Rendering") || safeMsg.includes("steps") || safeMsg.includes("Sampling") || safeMsg.includes("KSampler")) {
newStage = "Comfy Backend - Sampling";
}
else if (safeMsg.includes("Downloading")) {
newStage = "Downloading";
}
else if (safeMsg.includes("Requesting") || safeMsg.includes("Waiting") || safeMsg.includes("Queue")) {
newStage = "Queueing";
}
else if (safeMsg.includes("Allocating") || safeMsg.includes("GPU Acquired") || safeMsg.includes("Loading") || safeMsg.includes("Executing") || safeMsg.includes("Workflow")) {
newStage = "Comfy Backend - Model Loading";
}
if ((currentStageName === "Comfy Backend - Model Loading" || currentStageName === "Comfy Backend - Sampling")
&& (safeMsg.includes("Requesting") || safeMsg.includes("Waiting") || safeMsg.includes("Queue"))) {
return;
}
if (newStage !== currentStageName && newStage !== "Processing") {
const now = Date.now();
const prevDuration = ((now - currentStageStartTime) / 1000).toFixed(1);
if (currentStageName !== "Task Queued" && currentStageName !== "Processing") {
addLog(`⏱️ ${currentStageName} finished in ${prevDuration}s.`);
}
currentStageName = newStage;
currentStageStartTime = now;
addLog(`🚀 Starting ${newStage}...`);
}
const logContent = document.getElementById('log-content');
const lastLog = logContent.lastElementChild;
const lastText = lastLog ? lastLog.innerText : "";
if (safeMsg.includes("Rendering image (GPU processing)") && !safeMsg.includes("steps")) {
const elapsed = ((Date.now() - currentStageStartTime) / 1000).toFixed(0);
const lastLine = logContent.lastElementChild ? logContent.lastElementChild.innerText : "";
const lastElapsedMatch = lastLine.match(/\((\d+)s\)/);
if (!lastLine.includes("Rendering image") || !lastElapsedMatch || parseInt(lastElapsedMatch[1]) !== parseInt(elapsed)) {
addLog(`🎨 Rendering image (GPU processing)... (${elapsed}s)`);
}
smartScroll();
return;
}
if (safeMsg.includes("steps")) {
if (currentStageName !== "Comfy Backend - Sampling") {
const now = Date.now();
const prevDuration = ((now - currentStageStartTime) / 1000).toFixed(1);
if (currentStageName === "Comfy Backend - Model Loading") {
addLog(`⏱️ Comfy Backend - Model Loading finished in ${prevDuration}s.`, "normal");
addLog(`🚀 Starting Comfy Backend - Sampling...`, "normal");
}
currentStageName = "Comfy Backend - Sampling";
currentStageStartTime = now;
}
const lastLine = logContent.lastElementChild;
if (!lastLine || !lastLine.innerText.includes(safeMsg)) {
addLog(`\u2003\u2003${safeMsg}`, "normal");
}
smartScroll();
return;
}
if (currentStageName === "Loading VRAM" && !safeMsg.includes("Executing") && (safeMsg.includes("Waiting for GPU") || safeMsg.includes("Queue for GPU"))) {
return;
}
if (safeMsg.includes("Loading") || safeMsg.includes("Swapping") || safeMsg.includes("Waiting") || safeMsg.includes("GPU Acquired") || safeMsg.includes("Queue") || safeMsg.includes("Executing") || safeMsg.includes("Sampling") || safeMsg.includes("KSampler") || currentStageName === "Comfy Backend - Sampling") {
const elapsed = ((Date.now() - currentStageStartTime) / 1000).toFixed(0);
const lastLine = logContent.lastElementChild;
let isNewSecond = true;
if (lastLine && lastLine.dataset.lastElapsed === elapsed) {
isNewSecond = false;
}
if (isNewSecond) {
const newLine = document.createElement('div');
newLine.className = 'log-line';
let displayMsg = safeMsg;
if (currentStageName === "Comfy Backend - Model Loading") {
displayMsg = "📦 Loading tensors to VRAM...";
} else if (currentStageName === "Comfy Backend - Sampling") {
displayMsg = "🎨 Running KSampler...";
}
newLine.innerText = `[${new Date().toLocaleTimeString('en-US', { hour12: false })}] ${displayMsg} (Elapsed: ${elapsed}s)`;
newLine.dataset.lastElapsed = elapsed;
logContent.appendChild(newLine);
if (logContent.childElementCount > 100) {
logContent.firstElementChild.remove();
}
}
}
else {
if (!lastText.includes(safeMsg)) {
addLog(safeMsg);
}
}
smartScroll()
},
onSuccess: async (imageBase64, returnedSeed, metrics, filename) => {
placeholderText.style.display = 'none';
console.log("🚨 DEBUG METRICS:", JSON.stringify(metrics));
// 🌟 新增:打印后端传过来的文件名
console.log("✅ 后端返回的正确文件名:", filename);
const now = Date.now();
const totalDuration = ((now - generateStartTime) / 1000).toFixed(1);
const nowForStage = Date.now();
const prevStageDuration = ((nowForStage - currentStageStartTime) / 1000).toFixed(1);
if (currentStageName === "Comfy Backend - Model Loading") {
addLog(`⏱️ Comfy Backend - Model Loading finished in ${prevStageDuration}s.`, "normal");
addLog(`🚀 Starting Comfy Backend - Sampling...`, "normal");
const realSteps = metrics?.steps || "?";
addLog(`🎨 Rendering: ${realSteps}/${realSteps} steps (100%)`, "normal");
const pureRender = metrics ? (metrics.workflow - metrics.load_time) : 0;
addLog(`⏱️ Comfy Backend - Sampling finished in ${pureRender > 0 ? pureRender.toFixed(2) : "0.0"}s.`, "normal");
} else if (currentStageName === "Comfy Backend - Sampling") {
addLog(`⏱️ Comfy Backend - Sampling finished in ${prevStageDuration}s.`, "normal");
} else if (currentStageName === "Queueing" || currentStageName === "Comfy Workflow") {
addLog(`⏱️ Task execution completed. Syncing final stages...`, "normal");
}
addLog("✅ Generation successful!", "highlight");
addLog(`⏱️ Total Wait Time: ${totalDuration}s`, "highlight");
if (metrics) {
addLog(`----------------------------------------`, "normal");
addLog(`📊 [Backend Precise Metrics]`, "highlight");
const dl = Number(metrics.download || 0);
const q = Number(metrics.queue || 0);
const wf = Number(metrics.workflow || 0);
const load = Number(metrics.load_time || 0);
const pureRender = wf - load > 0 ? (wf - load) : wf;
const serverComputeTotal = (dl + q + wf).toFixed(2);
const networkOverhead = Math.max(0, totalDuration - serverComputeTotal).toFixed(1);
const secPerStep = pureRender > 0 && metrics.steps > 0 ? (pureRender / metrics.steps).toFixed(2) : 0;
const metricsLines = [
`\u2003\u2003⬇️ Resource Download: ${dl.toFixed(2)}s`,
`\u2003\u2003⏳ GPU Queue: ${q.toFixed(2)}s`,
load > 0 ? `\u2003\u2003📦 Model Loading: ${load.toFixed(2)}s` : null,
`\u2003\u2003🎨 Pure Compute (Sampler & Decode): ${pureRender.toFixed(2)}s (${metrics.steps} steps)`,
secPerStep > 0 ? `\u2003\u2003🚀 Average Speed: ${secPerStep}s / step` : null,
`\u2003\u2003🌐 Network Overhead: ~${networkOverhead}s`,
`\u2003\u2003⚙️ Server Compute Total: ${serverComputeTotal}s`
].filter(Boolean);
// 启动“打字机”
let currentIndex = 0;
const typeMetrics = () => {
if (currentIndex < metricsLines.length) {
requestAnimationFrame(() => {
addLog(metricsLines[currentIndex], "normal");
smartScroll(); // 🌟 强制触发一次滚动检查
currentIndex++;
setTimeout(typeMetrics, 120);
});
} else {
setTimeout(() => {
addLog(`----------------------------------------`, "normal");
smartScroll(); // 🌟 收尾也滚一下
if (returnedSeed !== undefined && returnedSeed !== null) {
addLog(`🎲 Random Seed resolved to: ${returnedSeed}`, "highlight");
smartScroll(); // 🌟 Seed 出来也滚一下
}
}, 300);
}
};
typeMetrics();
} else {
addLog(`⚠️ [Backend Metrics Missing]`, "error");
}
// ==========================================
// 🌟 第三阶段:图像渲染与收尾(极简模式)
// ==========================================
setTimeout(() => {
resultImage.onload = () => {
resultImage.style.display = 'block';
setUIState(false);
resultImage.onload = null;
};
// 🌟 核心:因为是 URL 了,加个时间戳防止浏览器缓存旧图
if (imageBase64.startsWith('data:')) {
resultImage.src = imageBase64;
} else {
resultImage.src = `${imageBase64}?t=${Date.now()}`;
}
}, 100);
},
onError: (msg) => {
let shortMsg = msg;
if (msg && msg.length > 60) {
shortMsg = msg.substring(0, 60) + "...";
}
placeholderText.style.display = 'block';
placeholderText.innerText = `❌ Failed: ${shortMsg}`;
setUIState(false);
addLog(`❌ Error: ${msg}`, "error");
}
});
} catch (error) {
placeholderText.style.display = 'block';
placeholderText.innerText = `❌ Request failed: ${error.message}`;
setUIState(false);
addLog(`❌ Request Error: ${error.message}`, "error");
}
});
// ==========================================================================
// 🚀 Initialization (带双重执行拦截锁)
// ==========================================================================
let isAppInitialized = false; // 🌟 全局锁
document.addEventListener("DOMContentLoaded", async () => {
// 🌟 核心修复:如果已经初始化过了,直接 return,拦截掉第二次的幽灵执行
if (isAppInitialized) {
console.log("⚠️ [Init] 拦截到重复的 DOMContentLoaded,已忽略。");
return;
}
isAppInitialized = true;
setTimeout(() => {
window._isDataReady = true; // 10秒后强行标记为就绪
}, 10000);
console.log("🔥 触发初始化...");
setUIState(false);
try {
const isHealthy = await BackendAPI.checkHealth();
if (isHealthy) {
console.log("✅ 后端在线,开始拉取数据");
backendState.isReady = true;
document.getElementById('backend-status-dot').className = "status-dot online";
setUIState(false);
await loadInitialData();
} else {
console.warn("⚠️ 首次心跳失败,启动后台轮询...");
document.getElementById('backend-status-dot').className = "status-dot offline";
startHealthPolling();
await loadInitialData();
}
} catch (error) {
console.error("❌ 初始化发生严重错误:", error);
document.getElementById('backend-status-dot').className = "status-dot offline";
startHealthPolling();
}
});
// ==========================================================================
// 📝 TODO (Architecture / Enhancements)
// ==========================================================================
// 1. [Prompt Presets] 实现基于 ID 的 Prompt 远程动态加载
// - 方案:将超长 Prompt 抽离为集中的 JSON 文件(如: prompt-assets/presets.json)。
// - 逻辑:图库 (Gallery) 跳转时仅传递 ?id=xxx,前端 (Studio) 接收到 ID 后,跨域 fetch 该 JSON 并自动填充 Prompt 输入框。
// - 优点:彻底解决 URL 长度限制,实现前后端数据彻底解耦,方便统一维护。
// commit@70f742a386b9cbbfd6df774b19d727485ef5446f
// ==========================================================================
// 🛡️ Dynamic API Base URL
// ==========================================================================
const hostname = window.location.hostname;
let API_BASE_URL = "";
// 1. 如果在 GUI 预览环境(哪怕带 .static 后缀)
if (hostname.includes("universal-lora-studio-gui")) {
API_BASE_URL = "https://waveshaker-universal-lora-studio-test.hf.space";
}
// 2. 如果在本地开发环境
else if (hostname === "localhost" || hostname === "127.0.0.1") {
API_BASE_URL = "https://waveshaker-universal-lora-studio-test.hf.space";
}
// 3. 如果在测试环境(test Space)
else if (hostname.includes("universal-lora-studio-test")) {
API_BASE_URL = ""; // 同源,走相对路径
}
// 4. 如果在线上环境(prod Space)
else if (hostname.includes("universal-lora-studio.hf.space") || hostname.includes("universal-lora-studio.static")) {
API_BASE_URL = ""; // 同源,走相对路径
}
// ==========================================================================
// 🔐 ZeroGPU Token Handshake
// ==========================================================================
const ZEROGPU_HEADERS_MSG = "supports-zerogpu-headers";
// 监听 HF 父窗口发来的握手信号
window.addEventListener("message", (event) => {
if (event.data === ZEROGPU_HEADERS_MSG) {
window.supports_zerogpu_headers = true;
}
});
// 向父窗口声明支持,触发后续的 token 索要流程
(function announceZeroGpuSupport() {
const hostname = window.location.hostname;
if (!hostname.endsWith(".hf.space") && !hostname.includes(".dev.")) {
return; // 非 HF 环境,不执行
}
const origin = hostname.includes(".dev.")
? `https://moon-${hostname.split(".")[1]}.dev.spaces.huggingface.tech`
: "https://huggingface.co";
try {
window.parent.postMessage(ZEROGPU_HEADERS_MSG, origin);
} catch (e) {
console.warn("ZeroGPU handshake failed:", e);
}
})();
// 向父窗口索要 x-ip-token(带 2 秒超时兜底)
async function requestZeroGpuToken() {
if (!window.supports_zerogpu_headers) return null;
const hostname = window.location.hostname;
if (!hostname.endsWith(".hf.space") && !hostname.includes(".dev.")) return null;
const origin = hostname.includes(".dev.")
? `https://moon-${hostname.split(".")[1]}.dev.spaces.huggingface.tech`
: "https://huggingface.co";
try {
return await new Promise((resolve) => {
const channel = new MessageChannel();
const timeout = setTimeout(() => {
channel.port1.close();
resolve(null);
}, 2000);
channel.port1.onmessage = ({ data }) => {
clearTimeout(timeout);
resolve(data || null);
};
window.parent.postMessage("zerogpu-headers", origin, [channel.port2]);
});
} catch {
return null;
}
}
// ==========================================================================
// 🛡️ API Layer
// ==========================================================================
const BackendAPI = {
// 1. 心跳检测
checkHealth: async function() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);