apolinario commited on
Commit
439049f
·
1 Parent(s): 0c45e74

Restore usage tracker, backed by real ZeroGPU quota endpoint

Browse files
Files changed (1) hide show
  1. 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);