feat(setup): show live download rate + size-remaining, surface HF token as a speed lever (#657)

The first-run Models & Engines page showed only "downloading…" (and, once totals
arrived, a bare percent + ETA). Users asked to see the actual download rate, the
size remaining, and a way to speed downloads up.

The backend already streams per-file byte counts and a windowed rate over SSE —
the UI just wasn't surfacing it. Changes (frontend-only):

- aggregate() now also returns live rate + bytes-remaining (was pct + ETA only),
  and is exported so the speed/remaining math is unit-tested.
- The download line now reads e.g. "38% · 5.2 MB/s · 1.2 GB left · ~3m", each
  part shown only once the stream has it (still degrades to "downloading…" early).
- New fmtBytes()/fmtRate() helpers (MB/GB, MB/s↔KB/s).

The Hugging Face token field already existed but was buried in an "advanced"
fold and framed only as "unlocks gated models" — so users hunting for a faster
download never found it. Reframed the title/hint to lead with what they want:
authenticated downloads are faster, have higher rate limits, and stall less
(and still unlock gated models like pyannote diarization). Token persistence and
the segmented/faster downloader (segmented_download.py) are unchanged — this just
makes the existing speed levers visible.

Test: frontend/src/test/wizardLibraryAggregate.test.js — aggregate sums bytes,
ignores completed-file rate, returns nulls before totals; fmtBytes/fmtRate
formatting + idle blanks.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Palash Debnath
2026-06-24 02:54:18 +05:30
committed by GitHub
co-authored by mergetest Claude Opus 4.8
parent 252f0d4fac
commit e87c13e919
3 changed files with 91 additions and 10 deletions
+40 -8
View File
@@ -23,6 +23,24 @@ import { listEngines, selectEngine } from '../api/engines';
const fmtGB = (gb) => (gb == null ? '' : `${gb.toFixed(gb < 10 ? 1 : 0)} GB`);
/** Human-readable byte size, e.g. 734003200 -> "700 MB", 1610612736 -> "1.5 GB". */
export function fmtBytes(bytes) {
if (!Number.isFinite(bytes) || bytes <= 0) return '';
const mb = bytes / (1024 * 1024);
if (mb < 1024) return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
const gb = mb / 1024;
return `${gb < 10 ? gb.toFixed(1) : Math.round(gb)} GB`;
}
/** Instantaneous download rate, e.g. 5452595 -> "5.2 MB/s". Blank when idle. */
export function fmtRate(bytesPerSec) {
if (!Number.isFinite(bytesPerSec) || bytesPerSec <= 0) return '';
const mb = bytesPerSec / (1024 * 1024);
if (mb >= 1) return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB/s`;
const kb = bytesPerSec / 1024;
return `${Math.max(1, Math.round(kb))} KB/s`;
}
/**
* A model is a "platform pick" when it explicitly targets one of THIS host's
* platform tags (the MLX mac-ARM speedups, CUDA-tuned variants, …) — the best
@@ -38,19 +56,21 @@ export function isPlatformPick(model, platformTags) {
);
}
/** Aggregate one repo's SSE file events: percent done + ETA from rates. */
function aggregate(files) {
/** Aggregate one repo's SSE file events: percent + ETA + live rate + remaining.
* Exported (pure) so the speed/remaining math is unit-testable. */
export function aggregate(files) {
let done = 0;
let total = 0;
let rate = 0;
for (const f of Object.values(files)) {
for (const f of Object.values(files || {})) {
done += f.downloaded || 0;
total += f.total || 0;
if ((f.total || 0) > (f.downloaded || 0)) rate += f.rate || 0;
}
const pct = total > 0 ? Math.min(100, Math.round((done / total) * 100)) : null;
const etaSec = rate > 0 && total > done ? (total - done) / rate : null;
return { pct, etaSec };
const remaining = total > done ? total - done : null;
const etaSec = rate > 0 && remaining ? remaining / rate : null;
return { pct, etaSec, rate, remaining };
}
function formatEta(seconds) {
@@ -198,8 +218,21 @@ export default function WizardLibrary() {
const modelRow = (m, chip, chipTone, note) => {
const p = progress[m.repo_id];
const { pct, etaSec } = p ? aggregate(p.files) : { pct: null, etaSec: null };
const { pct, etaSec, rate, remaining } = p
? aggregate(p.files)
: { pct: null, etaSec: null, rate: 0, remaining: null };
const downloading = !!p;
// Live telemetry line: "5.2 MB/s · 700 MB left · ~3m". Each part only shows
// once the SSE stream has the data, so early on it degrades to "downloading…".
const rateStr = fmtRate(rate);
const remainStr = fmtBytes(remaining);
const etaStr = etaSec != null ? formatEta(etaSec) : '';
const statParts = [
pct != null ? `${pct}%` : null,
rateStr || null,
remainStr ? t('firstrun.size_left', { size: remainStr, defaultValue: '{{size}} left' }) : null,
etaStr ? t('firstrun.eta_left', { eta: etaStr, defaultValue: '~{{eta}} left' }) : null,
].filter(Boolean);
return (
<Row
key={m.repo_id}
@@ -215,8 +248,7 @@ export default function WizardLibrary() {
<span className="swiz-lib__state"></span>
) : downloading ? (
<span className="swiz-lib__state swiz-lib__state--busy">
{pct != null ? `${pct}%` : t('firstrun.lib_downloading', 'downloading…')}
{etaSec != null && ` · ${t('firstrun.eta_left', { eta: formatEta(etaSec), defaultValue: '~{{eta}} left' })}`}
{statParts.length ? statParts.join(' · ') : t('firstrun.lib_downloading', 'downloading…')}
</span>
) : (
<button type="button" className="frs-btn frs-btn--quiet swiz-lib__act" onClick={() => install(m.repo_id)}>
+3 -2
View File
@@ -1946,10 +1946,11 @@
"trust_line": "Everything runs and stays on this machine — no account, no cloud, no telemetry.",
"resume_note": "Interrupted downloads resume automatically — closing the app is safe.",
"eta_left": "~{{eta}} left",
"size_left": "{{size}} left",
"first_sound_text": "Welcome to your studio. Every word you hear was generated on this machine, just now.",
"first_sound_done": "That voice? Generated seconds ago, locally. Welcome in.",
"hf_token_title": "Hugging Face token (optional)",
"hf_token_hint": "Unlocks gated models — e.g. speaker diarization for multi-speaker dubbing (pyannote). Free account token; stays on this machine.",
"hf_token_title": "Hugging Face token — faster, more reliable downloads (optional)",
"hf_token_hint": "A free account token gives authenticated downloads (faster, higher rate limits, fewer stalls) and unlocks gated models like speaker diarization for multi-speaker dubbing (pyannote). Stays on this machine.",
"hf_token_save": "Save",
"hf_token_saving": "saving…",
"hf_token_saved": "Hugging Face token saved",
@@ -0,0 +1,48 @@
import { describe, it, expect } from 'vitest';
import { aggregate, fmtBytes, fmtRate } from '../components/WizardLibrary.jsx';
describe('aggregate — download telemetry from SSE file events', () => {
it('sums bytes, computes pct, remaining, rate and ETA', () => {
const files = {
'a.bin': { downloaded: 500, total: 1000, rate: 100 },
'b.bin': { downloaded: 250, total: 1000, rate: 150 },
};
const { pct, remaining, rate, etaSec } = aggregate(files);
expect(pct).toBe(38); // 750 / 2000
expect(remaining).toBe(1250); // 2000 - 750
expect(rate).toBe(250); // both still downloading
expect(etaSec).toBeCloseTo(5, 5); // 1250 / 250
});
it('drops rate from already-complete files (no negative/idle ETA)', () => {
const files = {
done: { downloaded: 1000, total: 1000, rate: 999 }, // complete → rate ignored
live: { downloaded: 0, total: 1000, rate: 200 },
};
const { rate, remaining, etaSec } = aggregate(files);
expect(rate).toBe(200);
expect(remaining).toBe(1000);
expect(etaSec).toBeCloseTo(5, 5);
});
it('returns nulls before any totals arrive (degrades to "downloading…")', () => {
expect(aggregate({}).pct).toBeNull();
expect(aggregate({}).remaining).toBeNull();
expect(aggregate(undefined).pct).toBeNull();
});
});
describe('fmtBytes / fmtRate', () => {
it('formats remaining size in MB/GB', () => {
expect(fmtBytes(700 * 1024 * 1024)).toBe('700 MB');
expect(fmtBytes(1.5 * 1024 * 1024 * 1024)).toBe('1.5 GB');
expect(fmtBytes(0)).toBe('');
expect(fmtBytes(null)).toBe('');
});
it('formats rate in MB/s or KB/s, blank when idle', () => {
expect(fmtRate(5.2 * 1024 * 1024)).toBe('5.2 MB/s');
expect(fmtRate(512 * 1024)).toBe('512 KB/s');
expect(fmtRate(0)).toBe('');
});
});