Spaces:
Running
Running
Commit ·
5f5d462
1
Parent(s): b7f0f1d
Show ZeroGPU execution-count (runs) quota from new endpoint field
Browse files- index.html +63 -20
index.html
CHANGED
|
@@ -1506,6 +1506,7 @@ let resultAudioUrl = null;
|
|
| 1506 |
let modalFileData = null;
|
| 1507 |
let history = JSON.parse(localStorage.getItem("zl_history") || "[]");
|
| 1508 |
let quotaTimer = null;
|
|
|
|
| 1509 |
|
| 1510 |
// ── IndexedDB ──
|
| 1511 |
|
|
@@ -1704,6 +1705,17 @@ function fmtDuration(sec) {
|
|
| 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");
|
|
@@ -1713,24 +1725,39 @@ async function refreshQuota() {
|
|
| 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
|
| 1719 |
-
const
|
| 1720 |
|
| 1721 |
const fill = document.getElementById("usage-fill");
|
| 1722 |
-
|
| 1723 |
-
|
| 1724 |
-
|
| 1725 |
-
|
| 1726 |
-
|
| 1727 |
-
|
| 1728 |
-
|
| 1729 |
-
|
| 1730 |
-
|
| 1731 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1732 |
}
|
| 1733 |
-
document.getElementById("usage-sub").textContent = sub;
|
| 1734 |
if (tracker) tracker.style.display = "";
|
| 1735 |
} catch (err) {
|
| 1736 |
if (tracker) tracker.style.display = "none";
|
|
@@ -1741,17 +1768,29 @@ async function refreshQuota() {
|
|
| 1741 |
|
| 1742 |
// Build the body of a status bar for an error. The ZeroGPU backend sends a
|
| 1743 |
// misleading seconds-based message to free users whose real blocker is the
|
| 1744 |
-
// daily
|
| 1745 |
-
// We reframe
|
| 1746 |
function quotaErrorBodyHtml(errMsg) {
|
| 1747 |
if (!errMsg.includes("GPU quota")) {
|
| 1748 |
return `<div class="status-text">${errMsg}</div>`;
|
| 1749 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1750 |
if (!isPro) {
|
| 1751 |
-
return `<div class="status-text">You've used
|
| 1752 |
<a class="status-cta upgrade" href="https://huggingface.co/subscribe/pro?from=ZeroGPU" target="_blank">🤗 Subscribe to HF Pro — 40 min/day of GPU quota</a>
|
| 1753 |
<div style="font-size:11px;color:var(--text-muted);margin-top:2px">Your free quota resets every 24h</div>`;
|
| 1754 |
}
|
|
|
|
| 1755 |
let html = `<div class="status-text">${errMsg}</div>
|
| 1756 |
<a class="status-cta credits" href="https://huggingface.co/settings/billing" target="_blank">Buy additional GPU credits — $1 per 10 min</a>`;
|
| 1757 |
const m = errMsg.match(/Try again in ([\d:]+)/);
|
|
@@ -1761,10 +1800,11 @@ function quotaErrorBodyHtml(errMsg) {
|
|
| 1761 |
return html;
|
| 1762 |
}
|
| 1763 |
|
| 1764 |
-
function showStatusWithCTA(errorMsg) {
|
| 1765 |
const el = document.getElementById("status");
|
| 1766 |
el.className = "status-bar visible error";
|
| 1767 |
el.innerHTML = quotaErrorBodyHtml(errorMsg);
|
|
|
|
| 1768 |
}
|
| 1769 |
|
| 1770 |
function showStatus(type, msg) {
|
|
@@ -2149,10 +2189,11 @@ function showDesignStatus(type, msg) {
|
|
| 2149 |
el.innerHTML = `<div class="status-text">${msg}</div>`;
|
| 2150 |
}
|
| 2151 |
|
| 2152 |
-
function showDesignStatusWithCTA(errMsg) {
|
| 2153 |
const el = document.getElementById("design-status");
|
| 2154 |
el.className = "status-bar visible error";
|
| 2155 |
el.innerHTML = quotaErrorBodyHtml(errMsg);
|
|
|
|
| 2156 |
}
|
| 2157 |
|
| 2158 |
const designAudioEl = document.getElementById("design-audio-player");
|
|
@@ -2324,10 +2365,11 @@ function showTranscribeStatus(type, msg) {
|
|
| 2324 |
el.innerHTML = `<div class="status-text">${msg}</div>`;
|
| 2325 |
}
|
| 2326 |
|
| 2327 |
-
function showTranscribeStatusWithCTA(errMsg) {
|
| 2328 |
const el = document.getElementById("transcribe-status");
|
| 2329 |
el.className = "status-bar visible error";
|
| 2330 |
el.innerHTML = quotaErrorBodyHtml(errMsg);
|
|
|
|
| 2331 |
}
|
| 2332 |
|
| 2333 |
window.copyTranscript = function() {
|
|
@@ -2884,10 +2926,11 @@ function showStatusOn(elId, type, msg) {
|
|
| 2884 |
el.innerHTML = `<div class="status-text">${msg}</div>`;
|
| 2885 |
}
|
| 2886 |
|
| 2887 |
-
function showStatusCtaOn(elId, errMsg) {
|
| 2888 |
const el = document.getElementById(elId);
|
| 2889 |
el.className = "status-bar visible error";
|
| 2890 |
el.innerHTML = quotaErrorBodyHtml(errMsg);
|
|
|
|
| 2891 |
}
|
| 2892 |
|
| 2893 |
function wireUploadArea(areaId, inputId, labelId, onFile) {
|
|
|
|
| 1506 |
let modalFileData = null;
|
| 1507 |
let history = JSON.parse(localStorage.getItem("zl_history") || "[]");
|
| 1508 |
let quotaTimer = null;
|
| 1509 |
+
let lastQuota = null;
|
| 1510 |
|
| 1511 |
// ── IndexedDB ──
|
| 1512 |
|
|
|
|
| 1705 |
return m > 0 ? `${m}m ${s}s` : `${s}s`;
|
| 1706 |
}
|
| 1707 |
|
| 1708 |
+
function fmtResetsIn(iso) {
|
| 1709 |
+
if (!iso) return null;
|
| 1710 |
+
const ms = new Date(iso).getTime() - Date.now();
|
| 1711 |
+
if (isNaN(ms) || ms <= 0) return null;
|
| 1712 |
+
const mins = Math.round(ms / 60000);
|
| 1713 |
+
if (mins < 60) return `${mins}m`;
|
| 1714 |
+
const h = Math.floor(mins / 60);
|
| 1715 |
+
const m = mins % 60;
|
| 1716 |
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
| 1717 |
+
}
|
| 1718 |
+
|
| 1719 |
async function refreshQuota() {
|
| 1720 |
if (!accessToken) return;
|
| 1721 |
const tracker = document.getElementById("usage-tracker");
|
|
|
|
| 1725 |
});
|
| 1726 |
if (!res.ok) throw new Error(`quota ${res.status}`);
|
| 1727 |
const q = await res.json();
|
| 1728 |
+
lastQuota = q;
|
| 1729 |
+
|
| 1730 |
const base = q.base || 0;
|
| 1731 |
const remaining = Math.max(0, q.current || 0);
|
| 1732 |
+
const secUsed = Math.max(0, base - remaining);
|
| 1733 |
+
const runs = q.runs;
|
| 1734 |
|
| 1735 |
const fill = document.getElementById("usage-fill");
|
| 1736 |
+
const value = document.getElementById("usage-value");
|
| 1737 |
+
const sub = document.getElementById("usage-sub");
|
| 1738 |
+
|
| 1739 |
+
if (runs && runs.limit > 0) {
|
| 1740 |
+
// Free tier: the daily execution count is the binding limit — headline it.
|
| 1741 |
+
const pct = Math.min((runs.used / runs.limit) * 100, 100);
|
| 1742 |
+
fill.style.width = `${pct}%`;
|
| 1743 |
+
fill.className = "usage-fill" + (pct >= 100 ? " danger" : pct > 60 ? " warning" : "");
|
| 1744 |
+
value.textContent = `${Math.round(runs.used)}/${Math.round(runs.limit)} runs`;
|
| 1745 |
+
let line = `${Math.max(0, Math.round(runs.remaining))} free runs left · ${fmtDuration(remaining)} GPU`;
|
| 1746 |
+
const resetIn = fmtResetsIn(runs.resetsAt);
|
| 1747 |
+
if (resetIn) line += ` · resets in ${resetIn}`;
|
| 1748 |
+
sub.textContent = line;
|
| 1749 |
+
} else {
|
| 1750 |
+
// Pro / paid: GPU-seconds are the only limit.
|
| 1751 |
+
const pct = base > 0 ? Math.min((secUsed / base) * 100, 100) : 0;
|
| 1752 |
+
fill.style.width = `${pct}%`;
|
| 1753 |
+
fill.className = "usage-fill" + (pct > 80 ? " danger" : pct > 50 ? " warning" : "");
|
| 1754 |
+
value.textContent = `${fmtDuration(remaining)} left`;
|
| 1755 |
+
let line = `${fmtDuration(secUsed)} of ${fmtDuration(base)} ${isPro ? "Pro" : ""} GPU quota`.replace(/\s+/g, " ");
|
| 1756 |
+
if (q.overquotaUsed > 0) line += ` · +${fmtDuration(q.overquotaUsed)} over`;
|
| 1757 |
+
const resetIn = fmtResetsIn(q.resetsAt);
|
| 1758 |
+
if (resetIn) line += ` · resets in ${resetIn}`;
|
| 1759 |
+
sub.textContent = line;
|
| 1760 |
}
|
|
|
|
| 1761 |
if (tracker) tracker.style.display = "";
|
| 1762 |
} catch (err) {
|
| 1763 |
if (tracker) tracker.style.display = "none";
|
|
|
|
| 1768 |
|
| 1769 |
// Build the body of a status bar for an error. The ZeroGPU backend sends a
|
| 1770 |
// misleading seconds-based message to free users whose real blocker is the
|
| 1771 |
+
// daily execution count, and wrongly offers paid GPU credits (Pro-only).
|
| 1772 |
+
// We reframe using the real quota data (lastQuota.runs) and account tier.
|
| 1773 |
function quotaErrorBodyHtml(errMsg) {
|
| 1774 |
if (!errMsg.includes("GPU quota")) {
|
| 1775 |
return `<div class="status-text">${errMsg}</div>`;
|
| 1776 |
}
|
| 1777 |
+
const runs = lastQuota && lastQuota.runs;
|
| 1778 |
+
// Free tier hit the daily execution limit (runs present and exhausted).
|
| 1779 |
+
if (runs && runs.limit > 0 && runs.remaining <= 0) {
|
| 1780 |
+
const limit = Math.round(runs.limit);
|
| 1781 |
+
const resetIn = fmtResetsIn(runs.resetsAt);
|
| 1782 |
+
let html = `<div class="status-text">You've used all ${limit} free ZeroGPU runs for today.</div>
|
| 1783 |
+
<a class="status-cta upgrade" href="https://huggingface.co/subscribe/pro?from=ZeroGPU" target="_blank">🤗 Subscribe to HF Pro — 40 min/day of GPU quota</a>`;
|
| 1784 |
+
if (resetIn) html += `<div style="font-size:11px;color:var(--text-muted);margin-top:2px">Your free runs reset in ${resetIn}</div>`;
|
| 1785 |
+
return html;
|
| 1786 |
+
}
|
| 1787 |
+
// Free tier with runs left, or unknown tier: ran out of GPU minutes.
|
| 1788 |
if (!isPro) {
|
| 1789 |
+
return `<div class="status-text">You've used your free daily ZeroGPU minutes.</div>
|
| 1790 |
<a class="status-cta upgrade" href="https://huggingface.co/subscribe/pro?from=ZeroGPU" target="_blank">🤗 Subscribe to HF Pro — 40 min/day of GPU quota</a>
|
| 1791 |
<div style="font-size:11px;color:var(--text-muted);margin-top:2px">Your free quota resets every 24h</div>`;
|
| 1792 |
}
|
| 1793 |
+
// Pro / paid: real seconds message + credits CTA (credits are Pro-only).
|
| 1794 |
let html = `<div class="status-text">${errMsg}</div>
|
| 1795 |
<a class="status-cta credits" href="https://huggingface.co/settings/billing" target="_blank">Buy additional GPU credits — $1 per 10 min</a>`;
|
| 1796 |
const m = errMsg.match(/Try again in ([\d:]+)/);
|
|
|
|
| 1800 |
return html;
|
| 1801 |
}
|
| 1802 |
|
| 1803 |
+
async function showStatusWithCTA(errorMsg) {
|
| 1804 |
const el = document.getElementById("status");
|
| 1805 |
el.className = "status-bar visible error";
|
| 1806 |
el.innerHTML = quotaErrorBodyHtml(errorMsg);
|
| 1807 |
+
if (errorMsg.includes("GPU quota")) { await refreshQuota(); el.innerHTML = quotaErrorBodyHtml(errorMsg); }
|
| 1808 |
}
|
| 1809 |
|
| 1810 |
function showStatus(type, msg) {
|
|
|
|
| 2189 |
el.innerHTML = `<div class="status-text">${msg}</div>`;
|
| 2190 |
}
|
| 2191 |
|
| 2192 |
+
async function showDesignStatusWithCTA(errMsg) {
|
| 2193 |
const el = document.getElementById("design-status");
|
| 2194 |
el.className = "status-bar visible error";
|
| 2195 |
el.innerHTML = quotaErrorBodyHtml(errMsg);
|
| 2196 |
+
if (errMsg.includes("GPU quota")) { await refreshQuota(); el.innerHTML = quotaErrorBodyHtml(errMsg); }
|
| 2197 |
}
|
| 2198 |
|
| 2199 |
const designAudioEl = document.getElementById("design-audio-player");
|
|
|
|
| 2365 |
el.innerHTML = `<div class="status-text">${msg}</div>`;
|
| 2366 |
}
|
| 2367 |
|
| 2368 |
+
async function showTranscribeStatusWithCTA(errMsg) {
|
| 2369 |
const el = document.getElementById("transcribe-status");
|
| 2370 |
el.className = "status-bar visible error";
|
| 2371 |
el.innerHTML = quotaErrorBodyHtml(errMsg);
|
| 2372 |
+
if (errMsg.includes("GPU quota")) { await refreshQuota(); el.innerHTML = quotaErrorBodyHtml(errMsg); }
|
| 2373 |
}
|
| 2374 |
|
| 2375 |
window.copyTranscript = function() {
|
|
|
|
| 2926 |
el.innerHTML = `<div class="status-text">${msg}</div>`;
|
| 2927 |
}
|
| 2928 |
|
| 2929 |
+
async function showStatusCtaOn(elId, errMsg) {
|
| 2930 |
const el = document.getElementById(elId);
|
| 2931 |
el.className = "status-bar visible error";
|
| 2932 |
el.innerHTML = quotaErrorBodyHtml(errMsg);
|
| 2933 |
+
if (errMsg.includes("GPU quota")) { await refreshQuota(); el.innerHTML = quotaErrorBodyHtml(errMsg); }
|
| 2934 |
}
|
| 2935 |
|
| 2936 |
function wireUploadArea(areaId, inputId, labelId, onFile) {
|