Spaces:
Running
Running
Commit ·
439049f
1
Parent(s): 0c45e74
Restore usage tracker, backed by real ZeroGPU quota endpoint
Browse files- index.html +102 -0
index.html
CHANGED
|
@@ -235,6 +235,46 @@ body {
|
|
| 235 |
.voice-card:hover .delete-voice { opacity: 1; }
|
| 236 |
.voice-card .delete-voice:hover { color: var(--danger); background: rgba(239,68,68,0.1); }
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
/* ── Content ── */
|
| 239 |
.content {
|
| 240 |
overflow-y: auto;
|
|
@@ -874,6 +914,16 @@ pre {
|
|
| 874 |
</button>
|
| 875 |
</div>
|
| 876 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 877 |
</div>
|
| 878 |
|
| 879 |
<div class="content">
|
|
@@ -1455,6 +1505,7 @@ let selectedVoice = null;
|
|
| 1455 |
let resultAudioUrl = null;
|
| 1456 |
let modalFileData = null;
|
| 1457 |
let history = JSON.parse(localStorage.getItem("zl_history") || "[]");
|
|
|
|
| 1458 |
|
| 1459 |
// ── IndexedDB ──
|
| 1460 |
|
|
@@ -1644,6 +1695,48 @@ document.addEventListener("click", () => {
|
|
| 1644 |
document.getElementById("voice-picker")?.classList.remove("visible");
|
| 1645 |
});
|
| 1646 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1647 |
// ── Smart Error CTAs ──
|
| 1648 |
|
| 1649 |
function showStatusWithCTA(errorMsg) {
|
|
@@ -2037,6 +2130,7 @@ window.generateDesign = async function() {
|
|
| 2037 |
result.classList.add("visible");
|
| 2038 |
showDesignStatus("success", `Generated in ${wallSec.toFixed(1)}s`);
|
| 2039 |
addToHistory(text, "Voice Design", wallSec, designAudioUrl);
|
|
|
|
| 2040 |
} else {
|
| 2041 |
const errMsg = data.output?.error || "Generation failed";
|
| 2042 |
showDesignStatusWithCTA(errMsg);
|
|
@@ -2225,6 +2319,7 @@ window.transcribe = async function() {
|
|
| 2225 |
document.getElementById("transcribe-text").textContent = transcript;
|
| 2226 |
result.style.display = "block";
|
| 2227 |
showTranscribeStatus("success", `Transcribed in ${wallSec.toFixed(1)}s`);
|
|
|
|
| 2228 |
addToHistory(transcript.slice(0, 120) + (transcript.length > 120 ? "…" : ""), `Transcribe (${transcribeFile.name})`, wallSec, null);
|
| 2229 |
} else {
|
| 2230 |
showTranscribeStatusWithCTA(data.output?.error || "Transcription failed");
|
|
@@ -2367,6 +2462,9 @@ function showApp() {
|
|
| 2367 |
renderPresets();
|
| 2368 |
renderVoicesPage();
|
| 2369 |
renderHistory();
|
|
|
|
|
|
|
|
|
|
| 2370 |
}
|
| 2371 |
|
| 2372 |
// ── Pages ──
|
|
@@ -3188,6 +3286,7 @@ window.voiceChange = async function() {
|
|
| 3188 |
setPlayerMeta("vc", `${vcSource.name} → ${vcTarget.name}`);
|
| 3189 |
result.classList.add("visible");
|
| 3190 |
showStatusOn("vc-status", "success", `Converted in ${wallClock.toFixed(1)}s`);
|
|
|
|
| 3191 |
addToHistory(`${vcSource.name} → ${vcTarget.name}`, "Voice Changer", wallClock, url);
|
| 3192 |
} catch (err) {
|
| 3193 |
showStatusCtaOn("vc-status", err.message);
|
|
@@ -3259,6 +3358,7 @@ window.voiceIsolate = async function() {
|
|
| 3259 |
setPlayerMeta("iso", isoFile.name);
|
| 3260 |
result.classList.add("visible");
|
| 3261 |
showStatusOn("iso-status", "success", `Done in ${wallClock.toFixed(1)}s`);
|
|
|
|
| 3262 |
addToHistory(isoFile.name, denoiseOnly ? "Voice Cleaner (noise)" : "Voice Cleaner (enhance)", wallClock, url);
|
| 3263 |
} catch (err) {
|
| 3264 |
showStatusCtaOn("iso-status", err.message);
|
|
@@ -3313,6 +3413,7 @@ window.soundEffect = async function() {
|
|
| 3313 |
setPlayerMeta("sfx", `Prompt: "${prompt.slice(0,40)}${prompt.length>40?"...":""}"`);
|
| 3314 |
result.classList.add("visible");
|
| 3315 |
showStatusOn("sfx-status", "success", `Generated in ${wallClock.toFixed(1)}s`);
|
|
|
|
| 3316 |
addToHistory(prompt, "Sound Effects", wallClock, url);
|
| 3317 |
} catch (err) {
|
| 3318 |
showStatusCtaOn("sfx-status", err.message);
|
|
@@ -3431,6 +3532,7 @@ window.generate = async function() {
|
|
| 3431 |
result.classList.add("visible");
|
| 3432 |
showStatus("success", `Generated in ${wallSec.toFixed(1)}s`);
|
| 3433 |
addToHistory(text, selectedVoice.name, wallSec, resultAudioUrl);
|
|
|
|
| 3434 |
} else {
|
| 3435 |
const errMsg = data.output?.error || "Generation failed";
|
| 3436 |
showStatusWithCTA(errMsg);
|
|
|
|
| 235 |
.voice-card:hover .delete-voice { opacity: 1; }
|
| 236 |
.voice-card .delete-voice:hover { color: var(--danger); background: rgba(239,68,68,0.1); }
|
| 237 |
|
| 238 |
+
/* ── Usage tracker ── */
|
| 239 |
+
.usage-tracker {
|
| 240 |
+
padding: 12px;
|
| 241 |
+
margin: 0 12px 12px;
|
| 242 |
+
background: var(--surface2);
|
| 243 |
+
border: 1px solid var(--border);
|
| 244 |
+
border-radius: 10px;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.usage-header {
|
| 248 |
+
display: flex;
|
| 249 |
+
align-items: center;
|
| 250 |
+
justify-content: space-between;
|
| 251 |
+
margin-bottom: 8px;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.usage-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); }
|
| 255 |
+
.usage-value { font-size: 11px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
|
| 256 |
+
|
| 257 |
+
.usage-bar {
|
| 258 |
+
width: 100%;
|
| 259 |
+
height: 4px;
|
| 260 |
+
background: var(--surface3);
|
| 261 |
+
border-radius: 2px;
|
| 262 |
+
overflow: hidden;
|
| 263 |
+
margin-bottom: 6px;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.usage-fill {
|
| 267 |
+
height: 100%;
|
| 268 |
+
background: var(--accent);
|
| 269 |
+
border-radius: 2px;
|
| 270 |
+
transition: width 0.5s ease;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.usage-fill.warning { background: var(--warning); }
|
| 274 |
+
.usage-fill.danger { background: var(--danger); }
|
| 275 |
+
|
| 276 |
+
.usage-sub { font-size: 10px; color: var(--text-muted); }
|
| 277 |
+
|
| 278 |
/* ── Content ── */
|
| 279 |
.content {
|
| 280 |
overflow-y: auto;
|
|
|
|
| 914 |
</button>
|
| 915 |
</div>
|
| 916 |
|
| 917 |
+
<!-- Usage Tracker -->
|
| 918 |
+
<div class="usage-tracker" id="usage-tracker">
|
| 919 |
+
<div class="usage-header">
|
| 920 |
+
<span class="usage-label">ZeroGPU Quota</span>
|
| 921 |
+
<span class="usage-value" id="usage-value">—</span>
|
| 922 |
+
</div>
|
| 923 |
+
<div class="usage-bar"><div class="usage-fill" id="usage-fill" style="width:0%"></div></div>
|
| 924 |
+
<div class="usage-sub" id="usage-sub">Loading…</div>
|
| 925 |
+
</div>
|
| 926 |
+
|
| 927 |
</div>
|
| 928 |
|
| 929 |
<div class="content">
|
|
|
|
| 1505 |
let resultAudioUrl = null;
|
| 1506 |
let modalFileData = null;
|
| 1507 |
let history = JSON.parse(localStorage.getItem("zl_history") || "[]");
|
| 1508 |
+
let quotaTimer = null;
|
| 1509 |
|
| 1510 |
// ── IndexedDB ──
|
| 1511 |
|
|
|
|
| 1695 |
document.getElementById("voice-picker")?.classList.remove("visible");
|
| 1696 |
});
|
| 1697 |
|
| 1698 |
+
// ── Usage Tracker (real ZeroGPU quota) ──
|
| 1699 |
+
|
| 1700 |
+
function fmtDuration(sec) {
|
| 1701 |
+
sec = Math.max(0, Math.round(sec));
|
| 1702 |
+
const m = Math.floor(sec / 60);
|
| 1703 |
+
const s = sec % 60;
|
| 1704 |
+
return m > 0 ? `${m}m ${s}s` : `${s}s`;
|
| 1705 |
+
}
|
| 1706 |
+
|
| 1707 |
+
async function refreshQuota() {
|
| 1708 |
+
if (!accessToken) return;
|
| 1709 |
+
const tracker = document.getElementById("usage-tracker");
|
| 1710 |
+
try {
|
| 1711 |
+
const res = await fetch("https://huggingface.co/api/spaces/zero-gpu/quota", {
|
| 1712 |
+
headers: { Authorization: `Bearer ${accessToken}` }
|
| 1713 |
+
});
|
| 1714 |
+
if (!res.ok) throw new Error(`quota ${res.status}`);
|
| 1715 |
+
const q = await res.json();
|
| 1716 |
+
const base = q.base || 0;
|
| 1717 |
+
const remaining = Math.max(0, q.current || 0);
|
| 1718 |
+
const used = Math.max(0, base - remaining);
|
| 1719 |
+
const pct = base > 0 ? Math.min((used / base) * 100, 100) : 0;
|
| 1720 |
+
|
| 1721 |
+
const fill = document.getElementById("usage-fill");
|
| 1722 |
+
fill.style.width = `${pct}%`;
|
| 1723 |
+
fill.className = "usage-fill" + (pct > 80 ? " danger" : pct > 50 ? " warning" : "");
|
| 1724 |
+
|
| 1725 |
+
document.getElementById("usage-value").textContent = `${fmtDuration(remaining)} left`;
|
| 1726 |
+
|
| 1727 |
+
let sub = `${fmtDuration(used)} of ${fmtDuration(base)} ${isPro ? "Pro" : "free"} GPU quota`;
|
| 1728 |
+
if (q.overquotaUsed > 0) sub += ` · +${fmtDuration(q.overquotaUsed)} over`;
|
| 1729 |
+
if (q.resetsAt) {
|
| 1730 |
+
const t = new Date(q.resetsAt);
|
| 1731 |
+
if (!isNaN(t)) sub += ` · resets ${t.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
|
| 1732 |
+
}
|
| 1733 |
+
document.getElementById("usage-sub").textContent = sub;
|
| 1734 |
+
if (tracker) tracker.style.display = "";
|
| 1735 |
+
} catch (err) {
|
| 1736 |
+
if (tracker) tracker.style.display = "none";
|
| 1737 |
+
}
|
| 1738 |
+
}
|
| 1739 |
+
|
| 1740 |
// ── Smart Error CTAs ──
|
| 1741 |
|
| 1742 |
function showStatusWithCTA(errorMsg) {
|
|
|
|
| 2130 |
result.classList.add("visible");
|
| 2131 |
showDesignStatus("success", `Generated in ${wallSec.toFixed(1)}s`);
|
| 2132 |
addToHistory(text, "Voice Design", wallSec, designAudioUrl);
|
| 2133 |
+
refreshQuota();
|
| 2134 |
} else {
|
| 2135 |
const errMsg = data.output?.error || "Generation failed";
|
| 2136 |
showDesignStatusWithCTA(errMsg);
|
|
|
|
| 2319 |
document.getElementById("transcribe-text").textContent = transcript;
|
| 2320 |
result.style.display = "block";
|
| 2321 |
showTranscribeStatus("success", `Transcribed in ${wallSec.toFixed(1)}s`);
|
| 2322 |
+
refreshQuota();
|
| 2323 |
addToHistory(transcript.slice(0, 120) + (transcript.length > 120 ? "…" : ""), `Transcribe (${transcribeFile.name})`, wallSec, null);
|
| 2324 |
} else {
|
| 2325 |
showTranscribeStatusWithCTA(data.output?.error || "Transcription failed");
|
|
|
|
| 2462 |
renderPresets();
|
| 2463 |
renderVoicesPage();
|
| 2464 |
renderHistory();
|
| 2465 |
+
refreshQuota();
|
| 2466 |
+
if (quotaTimer) clearInterval(quotaTimer);
|
| 2467 |
+
quotaTimer = setInterval(refreshQuota, 60000);
|
| 2468 |
}
|
| 2469 |
|
| 2470 |
// ── Pages ──
|
|
|
|
| 3286 |
setPlayerMeta("vc", `${vcSource.name} → ${vcTarget.name}`);
|
| 3287 |
result.classList.add("visible");
|
| 3288 |
showStatusOn("vc-status", "success", `Converted in ${wallClock.toFixed(1)}s`);
|
| 3289 |
+
refreshQuota();
|
| 3290 |
addToHistory(`${vcSource.name} → ${vcTarget.name}`, "Voice Changer", wallClock, url);
|
| 3291 |
} catch (err) {
|
| 3292 |
showStatusCtaOn("vc-status", err.message);
|
|
|
|
| 3358 |
setPlayerMeta("iso", isoFile.name);
|
| 3359 |
result.classList.add("visible");
|
| 3360 |
showStatusOn("iso-status", "success", `Done in ${wallClock.toFixed(1)}s`);
|
| 3361 |
+
refreshQuota();
|
| 3362 |
addToHistory(isoFile.name, denoiseOnly ? "Voice Cleaner (noise)" : "Voice Cleaner (enhance)", wallClock, url);
|
| 3363 |
} catch (err) {
|
| 3364 |
showStatusCtaOn("iso-status", err.message);
|
|
|
|
| 3413 |
setPlayerMeta("sfx", `Prompt: "${prompt.slice(0,40)}${prompt.length>40?"...":""}"`);
|
| 3414 |
result.classList.add("visible");
|
| 3415 |
showStatusOn("sfx-status", "success", `Generated in ${wallClock.toFixed(1)}s`);
|
| 3416 |
+
refreshQuota();
|
| 3417 |
addToHistory(prompt, "Sound Effects", wallClock, url);
|
| 3418 |
} catch (err) {
|
| 3419 |
showStatusCtaOn("sfx-status", err.message);
|
|
|
|
| 3532 |
result.classList.add("visible");
|
| 3533 |
showStatus("success", `Generated in ${wallSec.toFixed(1)}s`);
|
| 3534 |
addToHistory(text, selectedVoice.name, wallSec, resultAudioUrl);
|
| 3535 |
+
refreshQuota();
|
| 3536 |
} else {
|
| 3537 |
const errMsg = data.output?.error || "Generation failed";
|
| 3538 |
showStatusWithCTA(errMsg);
|