From efeb639ad7507ab43a8992333f730a43d6e7931c Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:51:07 +0200 Subject: [PATCH 1/3] fix(ai): download progress bytes/speed + voice progress-scale bug (#333 item 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Voice model download (VoiceModelDownloadModal, transformers.js pipeline): transformers.js's own onProgress payload is `{ progress: <0-100>, loaded: , total: }` (verified against its readResponse() source — progress = loaded/total*100). voiceCommandService.ts treated `progress` as if it were already a 0-1 fraction, so the existing `Math.min(0.95, pct)` safety clamp kicked in almost immediately (any progress value over 0.95%, i.e. after the very first chunk) and stayed there for the rest of the download, making the progress bar look stuck. Now derives the fraction from the payload's real loaded/total byte counts (falling back to progress/100 only when absent), and threads the real bytes through to the modal for a live "X MB of Y MB" + speed display. WebLLM text-model download (LocalAiDownloadProgress, services/localAiFacade): the installed @mlc-ai/web-llm's own progress callback exposes only a 0-1 fraction, no structured byte counts. Adds a machine-readable per-model size table (WEBLLM_MODEL_APPROX_MB, mirroring the free-text GB figures already in WEBLLM_SUPPORTED_MODELS' labels) and derives an approximate downloaded/total MB + speed from progress × that table — clearly labeled as an estimate, not measured telemetry, since no real byte-level data is obtainable here without patching library internals. Both UIs share services/downloadProgressFormat.ts for MB/speed formatting. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 14 +++ README.md | 8 +- .../settings/LocalAiDownloadProgress.tsx | 27 ++++- components/voice/VoiceModelDownloadModal.tsx | 43 +++++++ locales/ar/settings.json | 30 ++--- locales/de/settings.json | 4 + locales/el/settings.json | 30 ++--- locales/en/settings.json | 4 + locales/es/settings.json | 4 + locales/eu/settings.json | 30 ++--- locales/fa/settings.json | 30 ++--- locales/fi/settings.json | 30 ++--- locales/fr/settings.json | 4 + locales/he/settings.json | 30 ++--- locales/hu/settings.json | 30 ++--- locales/is/settings.json | 30 ++--- locales/it/settings.json | 4 + locales/ja/settings.json | 30 ++--- locales/ko/settings.json | 30 ++--- locales/pt/settings.json | 30 ++--- locales/ru/settings.json | 30 ++--- locales/sv/settings.json | 30 ++--- locales/zh/settings.json | 30 ++--- packages/ai-core/src/index.ts | 15 +++ public/locales/ar/bundle.json | 30 ++--- public/locales/de/bundle.json | 4 + public/locales/el/bundle.json | 30 ++--- public/locales/en/bundle.json | 4 + public/locales/es/bundle.json | 4 + public/locales/eu/bundle.json | 30 ++--- public/locales/fa/bundle.json | 30 ++--- public/locales/fi/bundle.json | 30 ++--- public/locales/fr/bundle.json | 4 + public/locales/he/bundle.json | 30 ++--- public/locales/hu/bundle.json | 30 ++--- public/locales/is/bundle.json | 30 ++--- public/locales/it/bundle.json | 4 + public/locales/ja/bundle.json | 30 ++--- public/locales/ko/bundle.json | 30 ++--- public/locales/pt/bundle.json | 30 ++--- public/locales/ru/bundle.json | 30 ++--- public/locales/sv/bundle.json | 30 ++--- public/locales/zh/bundle.json | 30 ++--- services/ai/inferenceProgressEmitter.ts | 49 +++++++- services/downloadProgressFormat.ts | 13 +++ services/localAiFacade.ts | 11 +- services/voice/voiceCommandService.ts | 20 +++- tests/unit/LocalAiDownloadProgress.test.tsx | 110 +++++++++++++++++- .../voice/VoiceModelDownloadModal.test.tsx | 43 ++++++- tests/unit/localAiFacade.test.ts | 4 +- .../services/downloadProgressFormat.test.ts | 29 +++++ .../services/inferenceProgressEmitter.test.ts | 52 +++++++++ .../voice/voiceCommandService.test.ts | 86 ++++++++++++++ types.ts | 6 + 54 files changed, 1023 insertions(+), 387 deletions(-) create mode 100644 services/downloadProgressFormat.ts create mode 100644 tests/unit/services/downloadProgressFormat.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d3e99d2..c116aa97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the race where an ordinary protected write could commit after a migration had already claimed ownership of the same store, which could otherwise land ciphertext under a superseded key/generation. (#339) +- **Downloaded/total size and speed in both model-download progress UIs** (#333 item 1). + `VoiceModelDownloadModal` now shows real byte counts and transfer speed, sourced directly from + transformers.js's own progress payload (`loaded`/`total` fields it was already receiving but + discarding). `LocalAiDownloadProgress` (WebLLM text models) shows an **approximate** downloaded/ + total MB and speed, derived from the existing 0-1 progress fraction × a new per-model known-size + table (`WEBLLM_MODEL_APPROX_MB`) — the installed `@mlc-ai/web-llm`'s own progress callback exposes + no structured byte counts, so this is clearly labeled as an estimate, not measured telemetry. ### Changed @@ -60,6 +67,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Migration verification no longer re-scans already-verified stores on resume**, and a batch that reports progress without advancing its durable cursor is now rejected instead of being able to replay the same records indefinitely. (#337) +- **Voice model download progress bar looked stuck at ~95% for most of the download.** + `voiceCommandService.ts`'s download progress handler treated transformers.js's `progress` field + as if it were already a 0-1 fraction; it's actually 0-100 (percent), so the existing + `Math.min(0.95, pct)` safety clamp kicked in almost immediately (any progress value over 0.95%, + i.e. after the very first chunk of the download) and stayed there until the download's final + "complete" dispatch. Now derives the fraction from the payload's real `loaded`/`total` byte + counts (falling back to `progress / 100` only when those are absent). (#333 item 1) ### Docs diff --git a/README.md b/README.md index 267e40fd..b5f890a0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ v1.26.0 IndexedDB v8 PWA v3.0 - i18n 19 locales — 2904 keys + i18n 19 locales — 2908 keys 6477+ tests / 532 files Codecov Coverage License MIT @@ -397,7 +397,7 @@ Infrastructure-level features that keep the app fast and extensible as projects ### 🌐 Full Multi-Language Support -Shipped UI locales with **2904 i18n keys** across all 19 languages — zero hardcoded user-facing strings: +Shipped UI locales with **2908 i18n keys** across all 19 languages — zero hardcoded user-facing strings: - 🇩🇪 **German** (Deutsch) - 🇬🇧 **English** @@ -506,7 +506,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and | **PDF Export** | jsPDF | Client-side, configurable PDF document generation | | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | -| **i18n** | Custom React Context (`I18nContext.tsx`) | 2904 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | +| **i18n** | Custom React Context (`I18nContext.tsx`) | 2908 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | | **Testing** | Vitest 4.x (6477+ tests / 532 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | @@ -708,7 +708,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt **Current test metrics (2026-07-30, CI-reported):** - **6477+ unit tests** across **532 test files** — all passing - Coverage thresholds: lines ≥ 74 · branches ≥ 60 · functions ≥ 67 · statements ≥ 72 — enforced in CI (see Codecov badge for live metrics) -- i18n: **2904 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) +- i18n: **2908 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) **CI-cloud-first workflow (recommended):** On constrained hardware run **`pnpm run lint && pnpm run i18n:check && pnpm run typecheck`** locally, then push and let CI handle coverage, E2E, Lighthouse, and Stryker. Authoritative numbers come from CI artifacts (Codecov, JUnit). After CI goes green, update the README badges and `AUDIT.md` quality-gate line from the reported metrics. See **[`docs/CI.md`](docs/CI.md) § Cloud CI-first vs local development** for the full post-merge doc-update checklist. diff --git a/components/settings/LocalAiDownloadProgress.tsx b/components/settings/LocalAiDownloadProgress.tsx index 2292f6b4..be941428 100644 --- a/components/settings/LocalAiDownloadProgress.tsx +++ b/components/settings/LocalAiDownloadProgress.tsx @@ -9,6 +9,7 @@ import { inferenceProgressEmitter, type WebLlmLoadProgress, } from '../../services/ai/inferenceProgressEmitter'; +import { formatMegabytes, formatMegabytesPerSecond } from '../../services/downloadProgressFormat'; import { abortActivePreload, retryLastPreload } from '../../services/localAiFacade'; export const LocalAiDownloadProgress: FC = () => { @@ -46,6 +47,22 @@ export const LocalAiDownloadProgress: FC = () => { ? `${progressPct}% ${t('settings.ai.localAi.downloadedLabel')}, ${etaText}` : `${progressPct}%`; + // QNBS-v3 (#333 item 1): approximate — see WebLlmLoadProgress's own doc comment. Only shown once + // both bounds are known (null whenever the active model id isn't in WEBLLM_MODEL_APPROX_MB). + const sizeText = + progress.loadedBytes != null && progress.totalBytes != null + ? t('settings.ai.localAi.downloadSize', { + loaded: formatMegabytes(progress.loadedBytes), + total: formatMegabytes(progress.totalBytes), + }) + : ''; + const speedText = + progress.bytesPerSecond != null + ? t('settings.ai.localAi.downloadSpeed', { + speed: formatMegabytesPerSecond(progress.bytesPerSecond), + }) + : ''; + function handleCancel() { // QNBS-v3: actually abort the in-flight preload/worker task — not just hide the modal. abortActivePreload(); @@ -138,10 +155,18 @@ export const LocalAiDownloadProgress: FC = () => { /> -
+
{progressPct}% {etaText && {etaText}}
+ {(sizeText || speedText) && ( +
+ {sizeText} + {speedText && {speedText}} +
+ )}