MegaPotatoAI commited on
Commit
da251c7
·
verified ·
1 Parent(s): fab7cea

Daily limit: continue on megapotato.app

Browse files
assets/fonts/mp-sans-bold.woff2 CHANGED
Binary files a/assets/fonts/mp-sans-bold.woff2 and b/assets/fonts/mp-sans-bold.woff2 differ
 
assets/fonts/mp-sans-regular.woff2 CHANGED
Binary files a/assets/fonts/mp-sans-regular.woff2 and b/assets/fonts/mp-sans-regular.woff2 differ
 
core.js CHANGED
@@ -87,7 +87,11 @@ export class Demo {
87
  this.hfUser = null;
88
  this.active = null;
89
  this.busy = false;
90
- this.limitedUntil = 0;
 
 
 
 
91
  this.turnstile = { widget: null, pending: null, ready: null };
92
  this.loginWallNoted = false;
93
  this.turnstileFailNoted = false;
@@ -97,10 +101,14 @@ export class Demo {
97
 
98
  // ------------------------------------------------------------------ links
99
 
100
- /** A megapotato.app link with this Space's attribution slug (the site reads ?s=, not UTM). */
101
- site(path = "/tool", content = "") {
 
 
 
102
  const s = content ? `hf-${this.slug}-${content}` : `hf-${this.slug}`;
103
- return `https://megapotato.app${path}?s=${s.slice(0, 40)}`;
 
104
  }
105
 
106
  // ------------------------------------------------------------------ start
@@ -109,7 +117,7 @@ export class Demo {
109
  const login = this.hfLoginState();
110
  this.hfUser = await login;
111
  try {
112
- this.config = await api("/config", { headers: this.hfHeaders() }, 10_000);
113
  } catch {
114
  this.configFailed = true;
115
  return null;
@@ -121,7 +129,7 @@ export class Demo {
121
 
122
  async refresh() {
123
  try {
124
- this.config = await api("/config", { headers: this.hfHeaders() }, 10_000);
125
  this.configFailed = false;
126
  } catch {
127
  // keep the last config
@@ -130,6 +138,23 @@ export class Demo {
130
  return this.config;
131
  }
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  /** When a limit ends (a refused run's retry time, or the allowance's reset), read the allowance again. */
134
  watchQuota() {
135
  clearTimeout(this.quotaTimer);
@@ -190,6 +215,20 @@ export class Demo {
190
  return Date.now() < this.limitedUntil || this.loginNeeded() || this.quotaOut();
191
  }
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  runsLeft() {
194
  return this.config?.me?.left ?? null;
195
  }
@@ -463,6 +502,8 @@ export class Demo {
463
  if (e.kind === "login" && this.config?.me) this.config.me = { logged_in: false, left: 0, retry_at: null };
464
  if (["daily", "hourly", "budget", "spend_limit"].includes(e.kind)) {
465
  this.limitedUntil = e.retry_at ?? Date.now() + 3600_000;
 
 
466
  this.watchQuota();
467
  }
468
  throw e;
@@ -664,17 +705,18 @@ export async function thumbnail(file, info, edge = 160) {
664
  // ---------------------------------------------------------------------- copy
665
 
666
  /**
667
- * Title and note for an error. `t`: { verb: "enhance", runs: "enhancements", site: (content) => url, inHub: bool }.
668
  * Returns [title, noteHtml].
669
  */
670
  export function errorCopy(err, demo, t) {
671
- const link = (content, text = "megapotato.app") => `<a href="${demo.site("/tool", content)}" target="_blank" rel="noopener">${text}</a>`;
672
  const perDay = `${demo.config?.jobs_per_day ?? 2}${NBSP}${t.runs}`;
673
  const loginHint =
674
  window.self !== window.top
675
  ? `Log in to Hugging Face (top right) for ${perDay} a day.`
676
  : `Open this demo on <a href="${t.spaceUrl}" target="_blank" rel="noopener">Hugging Face</a> and log in for ${perDay} a day.`;
677
- const more = `Want more? ${link("limit", "Sign up on megapotato.app")} for downloads and full access.`;
 
678
  const still = "You can still explore the examples.";
679
  const table = {
680
  network: ["Could not reach the service.", "Check your connection, then press Resume."],
@@ -727,3 +769,14 @@ export function errorCopy(err, demo, t) {
727
  };
728
  return table[err.kind] ?? table.server;
729
  }
 
 
 
 
 
 
 
 
 
 
 
 
87
  this.hfUser = null;
88
  this.active = null;
89
  this.busy = false;
90
+ this.limitedUntil = 0; // a refused run's deadline
91
+ this.limitKind = null; // and its reason: "daily", "hourly", "budget" or "spend_limit"
92
+ this.limitSeq = 0; // the newest /config request at the time of that refusal
93
+ this.configSeq = 0; // /config requests started
94
+ this.configApplied = 0; // the newest one whose answer was taken
95
  this.turnstile = { widget: null, pending: null, ready: null };
96
  this.loginWallNoted = false;
97
  this.turnstileFailNoted = false;
 
101
 
102
  // ------------------------------------------------------------------ links
103
 
104
+ /**
105
+ * A megapotato.app link with this Space's attribution slug (the site reads ?s=, not UTM). `scale` (1, 2, 4 or 8)
106
+ * is the factor the visitor chose here, so the site can open in the same mode; left out when none is chosen.
107
+ */
108
+ site(path = "/tool", content = "", scale = null) {
109
  const s = content ? `hf-${this.slug}-${content}` : `hf-${this.slug}`;
110
+ const mode = [1, 2, 4, 8].includes(Number(scale)) ? `&scale=${Number(scale)}` : "";
111
+ return `https://megapotato.app${path}?s=${s.slice(0, 40)}${mode}`;
112
  }
113
 
114
  // ------------------------------------------------------------------ start
 
117
  const login = this.hfLoginState();
118
  this.hfUser = await login;
119
  try {
120
+ await this.readConfig();
121
  } catch {
122
  this.configFailed = true;
123
  return null;
 
129
 
130
  async refresh() {
131
  try {
132
+ await this.readConfig();
133
  this.configFailed = false;
134
  } catch {
135
  // keep the last config
 
138
  return this.config;
139
  }
140
 
141
+ /**
142
+ * Read /config. Requests can overlap: an answer to an older request than the one already taken is dropped, so
143
+ * stale numbers never replace newer ones. A daily or hourly refusal gives way to a newer answer that shows runs
144
+ * left (a refund, a reset); a service refusal (budget, spend limit) keeps its own deadline.
145
+ */
146
+ async readConfig() {
147
+ const seq = ++this.configSeq;
148
+ const config = await api("/config", { headers: this.hfHeaders() }, 10_000);
149
+ if (seq < this.configApplied) return;
150
+ this.configApplied = seq;
151
+ this.config = config;
152
+ if (seq > this.limitSeq && (this.limitKind === "daily" || this.limitKind === "hourly") && config.me?.left > 0) {
153
+ this.limitedUntil = 0;
154
+ this.limitKind = null;
155
+ }
156
+ }
157
+
158
  /** When a limit ends (a refused run's retry time, or the allowance's reset), read the allowance again. */
159
  watchQuota() {
160
  clearTimeout(this.quotaTimer);
 
215
  return Date.now() < this.limitedUntil || this.loginNeeded() || this.quotaOut();
216
  }
217
 
218
+ /**
219
+ * Why no run can start now, the login wall aside: { kind: "quota", retryAt } when the visitor's own runs are used
220
+ * up (the allowance says so, or a daily/hourly refusal stands); { kind: "paused", reason } while a service refusal
221
+ * (budget, spend limit) stands; null otherwise.
222
+ */
223
+ limitState() {
224
+ const refused = Date.now() < this.limitedUntil ? this.limitKind : null;
225
+ // A service pause outranks the visitor's own allowance: while it stands, more runs would not help.
226
+ if (refused && refused !== "daily" && refused !== "hourly") return { kind: "paused", reason: refused };
227
+ if (this.quotaOut()) return { kind: "quota", retryAt: this.config.me.retry_at ?? null };
228
+ if (refused) return { kind: "quota", retryAt: this.limitedUntil };
229
+ return null;
230
+ }
231
+
232
  runsLeft() {
233
  return this.config?.me?.left ?? null;
234
  }
 
502
  if (e.kind === "login" && this.config?.me) this.config.me = { logged_in: false, left: 0, retry_at: null };
503
  if (["daily", "hourly", "budget", "spend_limit"].includes(e.kind)) {
504
  this.limitedUntil = e.retry_at ?? Date.now() + 3600_000;
505
+ this.limitKind = e.kind;
506
+ this.limitSeq = this.configSeq; // only an allowance read after this refusal can lift it
507
  this.watchQuota();
508
  }
509
  throw e;
 
705
  // ---------------------------------------------------------------------- copy
706
 
707
  /**
708
+ * Title and note for an error. `t`: { verb: "enhance", runs: "runs", spaceUrl, scale?: the chosen factor for site links }.
709
  * Returns [title, noteHtml].
710
  */
711
  export function errorCopy(err, demo, t) {
712
+ const link = (content, text = "megapotato.app") => `<a href="${demo.site("/tool", content, t.scale)}" target="_blank" rel="noopener">${text}</a>`;
713
  const perDay = `${demo.config?.jobs_per_day ?? 2}${NBSP}${t.runs}`;
714
  const loginHint =
715
  window.self !== window.top
716
  ? `Log in to Hugging Face (top right) for ${perDay} a day.`
717
  : `Open this demo on <a href="${t.spaceUrl}" target="_blank" rel="noopener">Hugging Face</a> and log in for ${perDay} a day.`;
718
+ const bonus = signupBonus(demo);
719
+ const more = `${link("limit", "Sign up on megapotato.app")}: ${bonus ? `${bonus}${NBSP}free chips` : "free chips"} to start, full-quality downloads.`;
720
  const still = "You can still explore the examples.";
721
  const table = {
722
  network: ["Could not reach the service.", "Check your connection, then press Resume."],
 
769
  };
770
  return table[err.kind] ?? table.server;
771
  }
772
+
773
+ /** The sign-up bonus the service reports, when it is a plain positive whole number (it goes into page text). */
774
+ export function signupBonus(demo) {
775
+ const n = demo.config?.api?.signup_bonus;
776
+ return Number.isInteger(n) && n > 0 && n < 1_000_000 ? n : null;
777
+ }
778
+
779
+ /** The way on when the free runs here are used up: the product itself, with this Space's attribution. */
780
+ export function limitActions(demo, scale = null) {
781
+ return [{ label: "Continue on megapotato.app ↗", href: demo.site("/tool", "limit", scale), primary: true }];
782
+ }
examples/thumbs/fairground_1941.webp CHANGED

Git LFS Details

  • SHA256: 55bf2051c531ba14e56e38bdba3cfe870bf14a1e496f29dc5b8988f2fa4c21ac
  • Pointer size: 130 Bytes
  • Size of remote file: 41.4 kB
examples/thumbs/street_stall.webp CHANGED

Git LFS Details

  • SHA256: 21ad72095020957d2e627c917f95d2eb535630d0c01356fe5c9d0d7e4d1bcfb1
  • Pointer size: 130 Bytes
  • Size of remote file: 71.2 kB
examples/thumbs/visitors_1930s.webp CHANGED

Git LFS Details

  • SHA256: 4bbad3fc5c3b611899794ffecee11201cf78c2bc53609a966b6c9afd70b0b159
  • Pointer size: 130 Bytes
  • Size of remote file: 40.6 kB
examples/thumbs/waiting_room.webp CHANGED

Git LFS Details

  • SHA256: fc282c4f667c4c8df42927c84643896127716e908e6879308004f57e2a368dd8
  • Pointer size: 130 Bytes
  • Size of remote file: 31.2 kB
examples/thumbs/washington_1910s.webp CHANGED

Git LFS Details

  • SHA256: c8d649a297f3fb5739af5bd703878f51053e042d4bb2d2d97aad359555b0460f
  • Pointer size: 130 Bytes
  • Size of remote file: 30.7 kB
ui.js CHANGED
@@ -1,6 +1,6 @@
1
  // Photo Enhancer (MegaPotato): enhance at the original size or 2×, compare two frames side by side.
2
 
3
- import { DemoError, Demo, INFO_KINDS, NBSP, dims, errorCopy, escapeHtml, isResumable, thumbnail, usd } from "./core.js";
4
 
5
  const SPACE_URL = "https://huggingface.co/spaces/MegaPotatoAI/Photo-Enhancer";
6
  const COPY = { verb: "enhance", runs: "runs", spaceUrl: SPACE_URL };
@@ -269,15 +269,17 @@ const demo = new Demo({
269
  },
270
  });
271
 
272
- function status({ busy = false, error = false, text, note = "", actions = [], at = null }) {
 
273
  clearInterval(state.tick);
274
  els.status.hidden = false;
275
  els.status.dataset.state = error ? "error" : busy ? "busy" : "idle";
 
276
  const acts = actions.length
277
  ? `<span class="actions">${actions
278
  .map((a, i) =>
279
  a.href
280
- ? `<a href="${a.href}" target="_blank" rel="noopener">${a.label}</a>`
281
  : `<button type="button" class="${a.primary ? "primary" : ""}" data-i="${i}">${a.label}</button>`,
282
  )
283
  .join("")}</span>`
@@ -294,7 +296,12 @@ function status({ busy = false, error = false, text, note = "", actions = [], at
294
 
295
  function showError(err) {
296
  const [title, note] = errorCopy(err, demo, COPY);
297
- if (INFO_KINDS.has(err.kind) && !demo.active?.op) return status({ text: title, note });
 
 
 
 
 
298
  const actions = [];
299
  if (isResumable(err) && demo.active) actions.push({ label: "Resume", primary: true, run: () => go(() => demo.continue()) });
300
  if (err.kind === "download" && demo.active?.op) actions.push({ label: "Load again", primary: true, run: () => go(() => demo.continue()) });
@@ -307,7 +314,7 @@ function showError(err) {
307
  update();
308
  },
309
  });
310
- status({ error: true, text: title, note, actions });
311
  }
312
 
313
  // ---------------------------------------------------------------------------- photo and plan
@@ -395,9 +402,63 @@ function update() {
395
  const working = demo.busy || state.presenting;
396
  const pending = !!demo.active && !working;
397
  els.run.disabled = working || (!pending && (!state.plan || demo.configFailed || limited));
398
- els.run.textContent = working ? "Enhancing…" : pending ? "Resume" : login ? "Log in to enhance" : limited ? "No runs left today" : "Enhance";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  }
400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  // ---------------------------------------------------------------------------- run
402
 
403
  async function go(step) {
@@ -443,13 +504,14 @@ async function present(done) {
443
  markExample(null);
444
  els.caption.innerHTML = `<strong>Your photo</strong><span class="meta numeric">${sizeName(op.factor)} · ${dims(op.out_w, op.out_h)}</span>`;
445
  const left = demo.runsLeft();
446
- const actions = [{ label: "Open megapotato.app ↗", href: demo.site("/tool", "get-png") }];
447
  if (phone())
448
  actions.push({ label: "View result", run: () => els.viewerWrap.scrollIntoView({ behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" }) });
449
  status({
450
  text: `Ready${left !== null ? ` · ${left} run${left === 1 ? "" : "s"} left today` : ""}`,
451
  note: "Upload your image on megapotato.app to download it.",
452
  actions,
 
453
  });
454
  }
455
 
@@ -529,6 +591,7 @@ function wire() {
529
  document.querySelectorAll('input[name="mode"]').forEach((r) =>
530
  r.addEventListener("change", () => {
531
  state.factor = Number(r.value);
 
532
  refreshPlan();
533
  }),
534
  );
 
1
  // Photo Enhancer (MegaPotato): enhance at the original size or 2×, compare two frames side by side.
2
 
3
+ import { DemoError, Demo, INFO_KINDS, NBSP, dims, errorCopy, escapeHtml, isResumable, limitActions, signupBonus, thumbnail, usd } from "./core.js";
4
 
5
  const SPACE_URL = "https://huggingface.co/spaces/MegaPotatoAI/Photo-Enhancer";
6
  const COPY = { verb: "enhance", runs: "runs", spaceUrl: SPACE_URL };
 
269
  },
270
  });
271
 
272
+ /** `kind` names what the line is about ("run", "info", "limit"), so that only its owner replaces or clears it. */
273
+ function status({ busy = false, error = false, text, note = "", actions = [], at = null, kind = "" }) {
274
  clearInterval(state.tick);
275
  els.status.hidden = false;
276
  els.status.dataset.state = error ? "error" : busy ? "busy" : "idle";
277
+ els.status.dataset.kind = kind;
278
  const acts = actions.length
279
  ? `<span class="actions">${actions
280
  .map((a, i) =>
281
  a.href
282
+ ? `<a class="${a.primary ? "primary" : ""}" href="${a.href}" target="_blank" rel="noopener">${a.label}</a>`
283
  : `<button type="button" class="${a.primary ? "primary" : ""}" data-i="${i}">${a.label}</button>`,
284
  )
285
  .join("")}</span>`
 
296
 
297
  function showError(err) {
298
  const [title, note] = errorCopy(err, demo, COPY);
299
+ if (INFO_KINDS.has(err.kind) && !demo.active?.op) {
300
+ // no runs left here: the way on is the product, as a button next to the message
301
+ if (err.kind === "daily" || err.kind === "hourly") return showLimit(err.retry_at ?? demo.limitedUntil);
302
+ if (err.kind === "budget" || err.kind === "spend_limit") return showPaused(err.kind); // the service's pause, in its own words
303
+ return status({ text: title, note, kind: "info" });
304
+ }
305
  const actions = [];
306
  if (isResumable(err) && demo.active) actions.push({ label: "Resume", primary: true, run: () => go(() => demo.continue()) });
307
  if (err.kind === "download" && demo.active?.op) actions.push({ label: "Load again", primary: true, run: () => go(() => demo.continue()) });
 
314
  update();
315
  },
316
  });
317
+ status({ error: true, text: title, note, actions, kind: "run" });
318
  }
319
 
320
  // ---------------------------------------------------------------------------- photo and plan
 
402
  const working = demo.busy || state.presenting;
403
  const pending = !!demo.active && !working;
404
  els.run.disabled = working || (!pending && (!state.plan || demo.configFailed || limited));
405
+ els.run.textContent = working ? "Enhancing…" : pending ? "Resume" : login ? "Log in to enhance" : limited ? (demo.limitState()?.kind === "paused" ? "Paused right now" : "No runs left today") : "Enhance";
406
+ syncLimitNotice();
407
+ }
408
+
409
+ /**
410
+ * A visitor with no runs left here (today's are used, or a run was just refused) sees where to continue, unless the
411
+ * status line is busy with something else: a login notice, a run, or a result.
412
+ */
413
+ function syncLimitNotice() {
414
+ const idle = !!demo.config && !demo.loginNeeded() && !demo.active && !demo.busy && !state.presenting;
415
+ const limit = idle ? demo.limitState() : null;
416
+ const mine = !els.status.hidden && (els.status.dataset.kind === "limit" || els.status.dataset.kind === "paused");
417
+ if (limit) {
418
+ if (!els.status.hidden && !mine) return; // a login notice, a run or a result is there
419
+ if (limit.kind === "quota") showLimit(limit.retryAt);
420
+ else showPaused(limit.reason);
421
+ } else if (mine && !demo.limited()) {
422
+ els.status.hidden = true; // runs are possible again
423
+ }
424
  }
425
 
426
+ /** The factor the visitor has chosen (1: original size, or 2); the site links carry it. */
427
+ function limitScale() {
428
+ return state.factor;
429
+ }
430
+
431
+ /**
432
+ * The limit line: the message, when a run comes back, the offer, and the product as the primary button. Drawn again
433
+ * only when something it shows changed (the time, the bonus, the factor in its links).
434
+ */
435
+ function showLimit(retryAt) {
436
+ const scale = limitScale();
437
+ const sig = `${scale ?? ""}|${retryAt ?? ""}|${signupBonus(demo) ?? ""}`;
438
+ if (!els.status.hidden && els.status.dataset.kind === "limit" && els.status.dataset.sig === sig) return;
439
+ const [text, note] = errorCopy(new DemoError("daily", { retry_at: retryAt || null }), demo, { ...COPY, scale });
440
+ redraw(sig, { text, note, actions: limitActions(demo, scale), kind: "limit" });
441
+ }
442
+
443
+ /** The service refused for its own reasons (budget, spend limit): say that, not that the visitor's runs are used. */
444
+ function showPaused(reason) {
445
+ if (!els.status.hidden && els.status.dataset.kind === "paused" && els.status.dataset.sig === reason) return;
446
+ const [text, note] = errorCopy(new DemoError(reason), demo, COPY);
447
+ redraw(reason, { text, note, kind: "paused" });
448
+ }
449
+
450
+ /** Draw a limit or pause line; a link that had the focus and is still the same link keeps it. */
451
+ function redraw(sig, line) {
452
+ const links = [...els.status.querySelectorAll("a")];
453
+ const at = links.indexOf(document.activeElement);
454
+ const href = at >= 0 ? links[at].getAttribute("href") : null;
455
+ status(line);
456
+ els.status.dataset.sig = sig;
457
+ const again = at >= 0 ? els.status.querySelectorAll("a")[at] : null;
458
+ if (again && again.getAttribute("href") === href) again.focus();
459
+ }
460
+
461
+
462
  // ---------------------------------------------------------------------------- run
463
 
464
  async function go(step) {
 
504
  markExample(null);
505
  els.caption.innerHTML = `<strong>Your photo</strong><span class="meta numeric">${sizeName(op.factor)} · ${dims(op.out_w, op.out_h)}</span>`;
506
  const left = demo.runsLeft();
507
+ const actions = [{ label: "Open megapotato.app ↗", href: demo.site("/tool", "get-png", op.factor) }];
508
  if (phone())
509
  actions.push({ label: "View result", run: () => els.viewerWrap.scrollIntoView({ behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" }) });
510
  status({
511
  text: `Ready${left !== null ? ` · ${left} run${left === 1 ? "" : "s"} left today` : ""}`,
512
  note: "Upload your image on megapotato.app to download it.",
513
  actions,
514
+ kind: "run",
515
  });
516
  }
517
 
 
591
  document.querySelectorAll('input[name="mode"]').forEach((r) =>
592
  r.addEventListener("change", () => {
593
  state.factor = Number(r.value);
594
+ update();
595
  refreshPlan();
596
  }),
597
  );