Spaces:
Running on Zero
Running on Zero
GPU spam guard (serialize + button lock), live progress tickers, describe-first layout, real-machine bindings, chart fix for custom VRAM
Browse files- app.py +2 -2
- engine/speed.py +8 -6
- static/app.js +56 -12
- static/index.html +7 -6
- static/style.css +22 -2
app.py
CHANGED
|
@@ -75,7 +75,7 @@ def api_lookup(payload: LookupIn):
|
|
| 75 |
return lookup(p.get("repo", ""), p, spec_from_payload(p))
|
| 76 |
|
| 77 |
|
| 78 |
-
@app.api(name="parse")
|
| 79 |
def api_parse(text: str = "") -> dict:
|
| 80 |
"""Messy machine description -> form fields, via the fine-tuned spec
|
| 81 |
parser (cn0303/fitcheck-spec-parser). ZeroGPU via the Gradio queue."""
|
|
@@ -83,7 +83,7 @@ def api_parse(text: str = "") -> dict:
|
|
| 83 |
return parse_specs(text)
|
| 84 |
|
| 85 |
|
| 86 |
-
@app.api(name="ask")
|
| 87 |
def api_ask(question: str, facts: str = "") -> dict:
|
| 88 |
"""Plain-English follow-up, grounded in the facts /api/advise returned.
|
| 89 |
|
|
|
|
| 75 |
return lookup(p.get("repo", ""), p, spec_from_payload(p))
|
| 76 |
|
| 77 |
|
| 78 |
+
@app.api(name="parse", concurrency_limit=1)
|
| 79 |
def api_parse(text: str = "") -> dict:
|
| 80 |
"""Messy machine description -> form fields, via the fine-tuned spec
|
| 81 |
parser (cn0303/fitcheck-spec-parser). ZeroGPU via the Gradio queue."""
|
|
|
|
| 83 |
return parse_specs(text)
|
| 84 |
|
| 85 |
|
| 86 |
+
@app.api(name="ask", concurrency_limit=1)
|
| 87 |
def api_ask(question: str, facts: str = "") -> dict:
|
| 88 |
"""Plain-English follow-up, grounded in the facts /api/advise returned.
|
| 89 |
|
engine/speed.py
CHANGED
|
@@ -96,12 +96,14 @@ def bandwidth_for_spec(spec, gpu_label: str = "") -> tuple[float | None, str]:
|
|
| 96 |
return _apple_bw(gpu_label or spec.gpu_label), "Apple unified memory (conservative M2-gen figure)"
|
| 97 |
if spec.gpu_vendor in ("nvidia", "amd", "intel") and spec.vram_gb > 0:
|
| 98 |
n = _norm(gpu_label or spec.gpu_label)
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
| 105 |
return None, ""
|
| 106 |
return None, ""
|
| 107 |
|
|
|
|
| 96 |
return _apple_bw(gpu_label or spec.gpu_label), "Apple unified memory (conservative M2-gen figure)"
|
| 97 |
if spec.gpu_vendor in ("nvidia", "amd", "intel") and spec.vram_gb > 0:
|
| 98 |
n = _norm(gpu_label or spec.gpu_label)
|
| 99 |
+
# pass 1: name + VRAM proximity (disambiguates 8 vs 16 GB variants);
|
| 100 |
+
# pass 2: name only — a custom VRAM override must not hide the chart.
|
| 101 |
+
for check_vram in (True, False):
|
| 102 |
+
for key, bw, vram in _bw_index():
|
| 103 |
+
if key and key in n:
|
| 104 |
+
if check_vram and vram and spec.vram_gb and abs(vram - spec.vram_gb) > 4:
|
| 105 |
+
continue
|
| 106 |
+
return bw, "vendor spec sheet"
|
| 107 |
return None, ""
|
| 108 |
return None, ""
|
| 109 |
|
static/app.js
CHANGED
|
@@ -167,11 +167,27 @@ function fillGpu() {
|
|
| 167 |
if (!list.length) { sel.style.display = "none"; sel.innerHTML = ""; }
|
| 168 |
else { sel.style.display = "block"; sel.innerHTML = list.map(g => `<option>${g}</option>`).join(""); }
|
| 169 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
function syncProviderForComputer() {
|
| 171 |
-
const
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
fillGpu();
|
| 176 |
}
|
| 177 |
|
|
@@ -569,20 +585,42 @@ async function drawRoofline(speed) {
|
|
| 569 |
host.innerHTML = s;
|
| 570 |
}
|
| 571 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
// ---- Paste box: the fine-tuned spec parser fills the form -----------------
|
| 573 |
async function parsePaste() {
|
| 574 |
const text = $("#paste").value.trim();
|
| 575 |
const hint = $("#parse-hint");
|
| 576 |
-
|
| 577 |
-
hint.textContent = "
|
|
|
|
|
|
|
|
|
|
| 578 |
try {
|
| 579 |
const client = await getClient();
|
| 580 |
const r = await client.predict("/parse", { text });
|
| 581 |
const d = Array.isArray(r.data) ? r.data[0] : r.data;
|
| 582 |
-
|
|
|
|
| 583 |
applyParsed(d, hint);
|
| 584 |
} catch (e) {
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
| 586 |
}
|
| 587 |
}
|
| 588 |
|
|
@@ -635,19 +673,25 @@ function wireAsk() {
|
|
| 635 |
c.addEventListener("click", () => { input.value = c.textContent; askQuestion(c.textContent); }));
|
| 636 |
}
|
| 637 |
|
|
|
|
| 638 |
async function askQuestion(question) {
|
| 639 |
question = (question || "").trim();
|
| 640 |
const box = $("#ask-answer");
|
| 641 |
-
if (!question || !box) return;
|
|
|
|
| 642 |
box.hidden = false;
|
| 643 |
-
box.innerHTML = `<div class="ans-loading"
|
| 644 |
-
|
| 645 |
try {
|
| 646 |
const a = await callAsk(question, JSON.stringify(lastAdvice || {}));
|
|
|
|
| 647 |
renderAnswer(box, a);
|
| 648 |
} catch (e) {
|
|
|
|
| 649 |
// Surface the real error, never a generic stand-in.
|
| 650 |
box.innerHTML = `<div class="ans-card ans-error"><h3>The explainer hit an error</h3><p>${e && e.message ? e.message : e}</p></div>`;
|
|
|
|
|
|
|
| 651 |
}
|
| 652 |
}
|
| 653 |
|
|
@@ -714,7 +758,7 @@ function init() {
|
|
| 714 |
$("#paste").addEventListener("input", maybeLiveUpdate);
|
| 715 |
$("#check-btn").addEventListener("click", check);
|
| 716 |
const pb = $("#parse-btn"); if (pb) pb.addEventListener("click", parsePaste);
|
| 717 |
-
|
| 718 |
$("#find-specs-body").innerHTML = findSpecsText();
|
| 719 |
detectHardware();
|
| 720 |
// Pre-filled share/preview links: ?go renders immediately; optional
|
|
|
|
| 167 |
if (!list.length) { sel.style.display = "none"; sel.innerHTML = ""; }
|
| 168 |
else { sel.style.display = "block"; sel.innerHTML = list.map(g => `<option>${g}</option>`).join(""); }
|
| 169 |
}
|
| 170 |
+
// Real machines only: a Windows laptop can't have Apple Silicon, a Pi can't
|
| 171 |
+
// take a desktop card. Impossible provider buttons get disabled per computer.
|
| 172 |
+
const PROVIDER_ALLOWED = {
|
| 173 |
+
"Windows laptop": ["none", "nvidia", "amd", "intel", "unsure"],
|
| 174 |
+
"Windows desktop": ["none", "nvidia", "amd", "intel", "unsure"],
|
| 175 |
+
"Linux PC": ["none", "nvidia", "amd", "intel", "unsure"],
|
| 176 |
+
"Mac": ["apple"],
|
| 177 |
+
"Mini PC / Raspberry Pi": ["none", "nvidia", "unsure"], // nvidia = Jetson boards
|
| 178 |
+
};
|
| 179 |
+
|
| 180 |
function syncProviderForComputer() {
|
| 181 |
+
const allowed = PROVIDER_ALLOWED[state.computer] || ["none", "nvidia", "amd", "intel", "apple", "unsure"];
|
| 182 |
+
$("#provider-seg").querySelectorAll(".seg-btn").forEach(b => {
|
| 183 |
+
const ok = allowed.includes(b.dataset.val);
|
| 184 |
+
b.classList.toggle("disabled", !ok);
|
| 185 |
+
b.disabled = !ok;
|
| 186 |
+
});
|
| 187 |
+
if (!allowed.includes(state.provider)) {
|
| 188 |
+
state.provider = allowed[0];
|
| 189 |
+
setActive("#provider-seg", state.provider);
|
| 190 |
+
}
|
| 191 |
fillGpu();
|
| 192 |
}
|
| 193 |
|
|
|
|
| 585 |
host.innerHTML = s;
|
| 586 |
}
|
| 587 |
|
| 588 |
+
// ---- Live progress ticker (gradio-style elapsed seconds) -------------------
|
| 589 |
+
function startTicker(el, base) {
|
| 590 |
+
const t0 = Date.now();
|
| 591 |
+
el.dataset.ticking = "1";
|
| 592 |
+
const tick = () => {
|
| 593 |
+
if (el.dataset.ticking !== "1") return;
|
| 594 |
+
const s = Math.round((Date.now() - t0) / 1000);
|
| 595 |
+
el.innerHTML = `<span class="spinner"></span> ${base} — ${s}s` +
|
| 596 |
+
(s > 8 ? " <span class='tick-note'>(cold start: the model is waking, up to ~1 min)</span>" : "");
|
| 597 |
+
setTimeout(tick, 500);
|
| 598 |
+
};
|
| 599 |
+
tick();
|
| 600 |
+
return () => { el.dataset.ticking = "0"; };
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
// ---- Paste box: the fine-tuned spec parser fills the form -----------------
|
| 604 |
async function parsePaste() {
|
| 605 |
const text = $("#paste").value.trim();
|
| 606 |
const hint = $("#parse-hint");
|
| 607 |
+
const btn = $("#parse-btn");
|
| 608 |
+
if (!text) { hint.textContent = "Type or paste something first."; return; }
|
| 609 |
+
if (btn.disabled) return; // one in-flight call, ever — GPU time is real money
|
| 610 |
+
btn.disabled = true;
|
| 611 |
+
const stop = startTicker(hint, "Reading your description");
|
| 612 |
try {
|
| 613 |
const client = await getClient();
|
| 614 |
const r = await client.predict("/parse", { text });
|
| 615 |
const d = Array.isArray(r.data) ? r.data[0] : r.data;
|
| 616 |
+
stop();
|
| 617 |
+
if (d.error) { hint.textContent = `Failed: ${d.error}`; return; }
|
| 618 |
applyParsed(d, hint);
|
| 619 |
} catch (e) {
|
| 620 |
+
stop();
|
| 621 |
+
hint.textContent = `Failed: ${e && e.message ? e.message : e}`;
|
| 622 |
+
} finally {
|
| 623 |
+
btn.disabled = false;
|
| 624 |
}
|
| 625 |
}
|
| 626 |
|
|
|
|
| 673 |
c.addEventListener("click", () => { input.value = c.textContent; askQuestion(c.textContent); }));
|
| 674 |
}
|
| 675 |
|
| 676 |
+
let askBusy = false;
|
| 677 |
async function askQuestion(question) {
|
| 678 |
question = (question || "").trim();
|
| 679 |
const box = $("#ask-answer");
|
| 680 |
+
if (!question || !box || askBusy) return;
|
| 681 |
+
askBusy = true;
|
| 682 |
box.hidden = false;
|
| 683 |
+
box.innerHTML = `<div class="ans-loading" id="ask-tick"></div>`;
|
| 684 |
+
const stop = startTicker($("#ask-tick"), "Thinking it through");
|
| 685 |
try {
|
| 686 |
const a = await callAsk(question, JSON.stringify(lastAdvice || {}));
|
| 687 |
+
stop();
|
| 688 |
renderAnswer(box, a);
|
| 689 |
} catch (e) {
|
| 690 |
+
stop();
|
| 691 |
// Surface the real error, never a generic stand-in.
|
| 692 |
box.innerHTML = `<div class="ans-card ans-error"><h3>The explainer hit an error</h3><p>${e && e.message ? e.message : e}</p></div>`;
|
| 693 |
+
} finally {
|
| 694 |
+
askBusy = false;
|
| 695 |
}
|
| 696 |
}
|
| 697 |
|
|
|
|
| 758 |
$("#paste").addEventListener("input", maybeLiveUpdate);
|
| 759 |
$("#check-btn").addEventListener("click", check);
|
| 760 |
const pb = $("#parse-btn"); if (pb) pb.addEventListener("click", parsePaste);
|
| 761 |
+
syncProviderForComputer();
|
| 762 |
$("#find-specs-body").innerHTML = findSpecsText();
|
| 763 |
detectHardware();
|
| 764 |
// Pre-filled share/preview links: ?go renders immediately; optional
|
static/index.html
CHANGED
|
@@ -45,6 +45,13 @@
|
|
| 45 |
<div class="step-head"><span class="step-num">1</span><h2>Your computer</h2></div>
|
| 46 |
<div class="hint" id="detect-hint" style="display:none; margin-bottom:var(--s-3)"></div>
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
<div class="field">
|
| 49 |
<span class="label">What kind of computer?</span>
|
| 50 |
<div class="segmented" id="computer-seg">
|
|
@@ -87,12 +94,6 @@
|
|
| 87 |
<input type="number" id="vram" placeholder="e.g. 8" min="0" max="200" step="1" />
|
| 88 |
<div class="hint">Overrides the GPU picker above. Leave blank to use the picker.</div>
|
| 89 |
</div>
|
| 90 |
-
<div class="field" style="margin-bottom:0">
|
| 91 |
-
<span class="label">Or paste / describe your specs</span>
|
| 92 |
-
<textarea id="paste" placeholder="Paste output from 'dxdiag' or 'Task Manager → Performance', or just describe it: 'Dell XPS, RTX 3050, 16GB'…"></textarea>
|
| 93 |
-
<button class="parse-btn" id="parse-btn">Fill the form from this</button>
|
| 94 |
-
<div class="hint" id="parse-hint">A small model fine-tuned for this reads your text and fills the form. It never guesses: anything you didn't say stays blank.</div>
|
| 95 |
-
</div>
|
| 96 |
</div>
|
| 97 |
</details>
|
| 98 |
</div>
|
|
|
|
| 45 |
<div class="step-head"><span class="step-num">1</span><h2>Your computer</h2></div>
|
| 46 |
<div class="hint" id="detect-hint" style="display:none; margin-bottom:var(--s-3)"></div>
|
| 47 |
|
| 48 |
+
<div class="field">
|
| 49 |
+
<span class="label">Describe it in your own words <span class="optional">(fastest)</span></span>
|
| 50 |
+
<textarea id="paste" placeholder="'Dell XPS, RTX 3050, 16GB' — or paste your dxdiag / Task Manager output…"></textarea>
|
| 51 |
+
<button class="parse-btn" id="parse-btn">Fill the form from this</button>
|
| 52 |
+
<div class="hint" id="parse-hint">A small model fine-tuned for this reads your text and fills the pickers below. It never guesses: anything you didn't say stays blank.</div>
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
<div class="field">
|
| 56 |
<span class="label">What kind of computer?</span>
|
| 57 |
<div class="segmented" id="computer-seg">
|
|
|
|
| 94 |
<input type="number" id="vram" placeholder="e.g. 8" min="0" max="200" step="1" />
|
| 95 |
<div class="hint">Overrides the GPU picker above. Leave blank to use the picker.</div>
|
| 96 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
</div>
|
| 98 |
</details>
|
| 99 |
</div>
|
static/style.css
CHANGED
|
@@ -137,6 +137,10 @@ button { font-family: inherit; cursor: pointer; }
|
|
| 137 |
box-shadow: var(--shadow-md);
|
| 138 |
}
|
| 139 |
.form-panel { padding: var(--s-5); position: sticky; top: var(--s-4); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
@media (max-width: 900px) { .form-panel { position: static; } }
|
| 141 |
|
| 142 |
.step { margin-bottom: var(--s-5); }
|
|
@@ -167,9 +171,23 @@ button { font-family: inherit; cursor: pointer; }
|
|
| 167 |
color: var(--text-primary); box-shadow: var(--shadow-sm);
|
| 168 |
}
|
| 169 |
.seg-btn .ic { font-size: 17px; color: var(--text-muted); }
|
| 170 |
-
.seg-btn.active .ic { color: var(--accent); }
|
| 171 |
.seg-btn .ic.brand { color: var(--text-secondary); }
|
| 172 |
-
.seg-btn.active .ic.brand { color: var(--text-primary); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
/* Native select / input styling */
|
| 175 |
select, input[type="text"], input[type="number"], textarea {
|
|
@@ -501,6 +519,8 @@ details.disc > summary:hover { color: var(--text-primary); }
|
|
| 501 |
.ans-card.ans-error p { color: var(--text-secondary); font-size: 13.5px; word-break: break-word; }
|
| 502 |
.ans-loading { display: flex; align-items: center; gap: var(--s-2); color: var(--text-muted); font-size: 14px; padding: var(--s-2) 0; flex-wrap: wrap; }
|
| 503 |
.ans-cold { font-size: 12px; color: var(--text-muted); opacity: .75; }
|
|
|
|
|
|
|
| 504 |
.spinner {
|
| 505 |
width: 15px; height: 15px; flex: none; border-radius: 50%;
|
| 506 |
border: 2px solid var(--border-hi); border-top-color: var(--accent);
|
|
|
|
| 137 |
box-shadow: var(--shadow-md);
|
| 138 |
}
|
| 139 |
.form-panel { padding: var(--s-5); position: sticky; top: var(--s-4); }
|
| 140 |
+
@media (min-width: 901px) {
|
| 141 |
+
/* tall forms (long descriptions, many pickers) scroll inside the panel */
|
| 142 |
+
.form-panel { max-height: calc(100vh - 32px); overflow-y: auto; scrollbar-width: thin; }
|
| 143 |
+
}
|
| 144 |
@media (max-width: 900px) { .form-panel { position: static; } }
|
| 145 |
|
| 146 |
.step { margin-bottom: var(--s-5); }
|
|
|
|
| 171 |
color: var(--text-primary); box-shadow: var(--shadow-sm);
|
| 172 |
}
|
| 173 |
.seg-btn .ic { font-size: 17px; color: var(--text-muted); }
|
| 174 |
+
.seg-btn.active .ic { color: var(--segc, var(--accent)); }
|
| 175 |
.seg-btn .ic.brand { color: var(--text-secondary); }
|
| 176 |
+
.seg-btn.active .ic.brand { color: var(--segc, var(--text-primary)); }
|
| 177 |
+
.seg-btn.disabled { opacity: .3; pointer-events: none; }
|
| 178 |
+
|
| 179 |
+
/* Each position gets its own accent when selected — coloured boundaries,
|
| 180 |
+
line icons stay line icons. */
|
| 181 |
+
.segmented .seg-btn:nth-child(1) { --segc: #60A5FA; }
|
| 182 |
+
.segmented .seg-btn:nth-child(2) { --segc: #4ADE80; }
|
| 183 |
+
.segmented .seg-btn:nth-child(3) { --segc: #F472B6; }
|
| 184 |
+
.segmented .seg-btn:nth-child(4) { --segc: #FBBF24; }
|
| 185 |
+
.segmented .seg-btn:nth-child(5) { --segc: #2DD4BF; }
|
| 186 |
+
.segmented .seg-btn:nth-child(6) { --segc: #C084FC; }
|
| 187 |
+
.seg-btn.active {
|
| 188 |
+
border-color: var(--segc, var(--accent)) !important;
|
| 189 |
+
background: color-mix(in srgb, var(--segc, var(--accent)) 13%, transparent);
|
| 190 |
+
}
|
| 191 |
|
| 192 |
/* Native select / input styling */
|
| 193 |
select, input[type="text"], input[type="number"], textarea {
|
|
|
|
| 519 |
.ans-card.ans-error p { color: var(--text-secondary); font-size: 13.5px; word-break: break-word; }
|
| 520 |
.ans-loading { display: flex; align-items: center; gap: var(--s-2); color: var(--text-muted); font-size: 14px; padding: var(--s-2) 0; flex-wrap: wrap; }
|
| 521 |
.ans-cold { font-size: 12px; color: var(--text-muted); opacity: .75; }
|
| 522 |
+
.tick-note { font-size: 12px; color: var(--text-muted); opacity: .8; }
|
| 523 |
+
#parse-hint .spinner, .ans-loading .spinner { display: inline-block; vertical-align: -2px; margin-right: 6px; }
|
| 524 |
.spinner {
|
| 525 |
width: 15px; height: 15px; flex: none; border-radius: 50%;
|
| 526 |
border: 2px solid var(--border-hi); border-top-color: var(--accent);
|