Commit d908bc4
feat: on-device runtime (residency + modality queue), dictation, HF-download fix, image gen (#39)
* perf(imagegen): persistent sd-server + taesd fast-VAE + shared defaults
Keep the stable-diffusion.cpp model resident across images (sd-server) so
2nd+ generations skip the ~13s Metal shader warmup + ~5s model reload, and
add opt-in TAESD fast VAE decode (verified 1.47s vs ~10-16s full VAE at 768).
Quality/steps are unchanged. Pure per-model defaults extracted to one shared
source of truth (image-defaults.ts) used by both the resident-server and
one-shot sd-cli paths. See docs/IMAGE_GEN_OPTIMIZATION.md for the full
benchmark study (incl. ruled-out levers: upstream rebuild = no speedup,
Winograd fork = unavailable, conv-direct = 9x slower).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(imagegen): record bake attempt status — runtime LoRA broken, merge works, GGUF handoff blocked
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imagegen): karras schedule + correct few-step defaults for distilled models
The crispness bug: distilled (Turbo/Lightning/DMD2) models were run on the
default 'discrete' sigma schedule, which undercooks few-step sigmas -> smeared
output; and turbo defaulted to 4 steps/cfg1 -> artifacts. Fix: distilled models
now default to 8 steps / cfg 2 / dpm++2m / KARRAS / 512, taesd on; full models
keep 28 steps / cfg 7 / discrete. Scheduler threaded through both the persistent
sd-server (sample_params.scheduler) and one-shot sd-cli (--scheduler). Measured:
crisp 5-star SDXL in ~12s warm at 512 on an M4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imagegen): finalize distilled default to 512/10/karras/full-VAE
512² (approved), 10 steps, cfg 2, dpm++2m, KARRAS, FULL VAE — ~30s warm at good
quality on an M4. taesd is now OFF by default (it softens detail and BLANKS at
1024 — the tiny decoder overflows); it stays strictly opt-in via fastVae for a
fast low-res draft. Only the two distilled models (dreamshaper-turbo,
realvis-lightning) get this fast path; the 5 non-distilled models still run
28 steps until they get distilled variants.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(models): surface 'Fast' distilled models first + emerald tag
Distilled few-step image models (tagged 'Fast' in the catalog: dreamshaper-turbo,
realvis-lightning, z-image-turbo, sdxl-lightning) render in ~30s vs ~100s for the
non-distilled ones. Sort them to the top of the model list (featureRank) and style
the Fast tag in the emerald brand accent so it reads as the recommended quick pick.
The 5 non-distilled models stay listed (get distilled variants later).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(imagegen): Fast/Quality tier toggle + single shared source of truth
Adds a Fast/Quality tier toggle to the image composer: Fast = 512/~30s (default),
Quality = model's native resolution (crisper, ~3x slower). Live progress bar +
forming-preview already existed.
Fixes a SOLID violation that was silently breaking the quality fix: the renderer
had a DUPLICATE, stale copy of the per-model defaults (defaulting turbo to 4 steps
-> rainbow artifacts) that overrode the main process. Collapsed both layers onto
one shared module (src/shared/image-defaults.ts, imported by main + renderer),
with a tier-aware resolveTier(). Production build verified (bundles into both).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imagegen): address PR review — on-demand LLM resume, poll timeout, seed, port-kill, docs
- CRITICAL (qodo): after a persistent-server image gen, llm.pause() left chat
dead until 60s idle eviction. llm.init() now evicts the resident image server
on-demand (via a hook, no layering violation) and never silently no-ops.
- sd-server poll loop: add a stall watchdog (abort if no progress for 180s) so a
hung server can't wedge generation forever.
- sd-server: surface the server-resolved seed so a random (-1) request stays
reproducible; fall back to the requested seed. Tested.
- killOrphanOnPort: match our bundled binary's full path, not the bare name, so a
user's own sd-server is never killed.
- docs: remove em dashes (brand voice rule).
- remove orphan duplicate src/main/image-defaults.ts (moved to src/shared).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(transcription): add Parakeet STT engine behind the transcription seam
Add NVIDIA Parakeet (sherpa-onnx offline transducer, ONNX) as a second
TranscriptionService alongside whisper.cpp, selected through a generic
engine-selection abstraction. Whisper stays the default; Parakeet is opt-in and
only used when its runtime (binary + model) is actually installed, otherwise the
selection falls back to whisper so a missing engine can never break dictation or
file transcription.
Core, engine-agnostic paths (voice:transcribe IPC, RAG/file audio ingest) route
through select.ts (getActiveTranscription) rather than importing whisper-cli
directly, so a Parakeet model selection is honored. whisper-cli guards the shared
active-transcription slot so a Parakeet ONNX id is never handed to whisper.
- src/main/transcription/parakeet-cli.ts — ParakeetCliTranscription engine
- src/main/transcription/select.ts — pure engine-selection + whisper fallback
- src/main/transcription/whisper-cli.ts — ggml-only guard on the shared active slot
- @offgrid/models — ModelEntry.engine field + two Parakeet catalog entries (v2/v3)
- src/main/ipc.ts, src/main/rag/extractors.ts — route via the active engine
- scripts/fetch-parakeet.sh + release.yml step — CI-stage the runtime (no-op until
SHERPA_ONNX_URL/PARAKEET_MODEL_URL are set), .gitignore the staged dir
- unit tests: parakeet-cli (arg/parse/match/availability) + select (fallback logic)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(models): don't tag Z-Image Turbo as 'Fast'
Despite the 'Turbo' name, Z-Image is a FLUX-class diffusion transformer (DiT +
Qwen3-4B text encoder + FLUX VAE) — heavy and slow on Apple Silicon via ggml, not
a few-step SDXL distill. Reserve the 'Fast' tag for models verified fast on-device
(dreamshaper-turbo, realvis-lightning). Fixed in source + the committed dist build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(imagegen): auto-generate when chat asks for an image ('draw a dog')
A natural-language image request in chat (draw/sketch/paint/render X, or
generate/create an image/picture of X) now routes to the image generator instead
of the text model refusing 'I can't draw images'. Conservative heuristic (pure +
tested): bare visual verbs count; weak verbs need an image noun; non-visual idioms
('draw a conclusion/attention') are excluded. Explicit image-mode is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imagegen): show image progress UI on auto-routed 'draw a dog' requests
When a chat message auto-routes to image generation, the global mode stays 'ask',
so the image progress/warm-up UI (gated on mode==='image') didn't show — the user
saw a chat-style loading state instead. Add a generatingImage flag so the
progress bar, forming preview, and Stop button engage whenever an image is
generating, regardless of mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imagegen): composer defaults to the ACTIVE image model (not a name heuristic)
The image composer's model picker defaulted via a name heuristic that matched the
parked Core ML dir (coreml-sdxl-base) on 'sdxl' — so it mismatched the active model
(DreamShaper), and because Core ML is non-distilled, FAST showed 28 steps and the
FAST/QUALITY toggle did nothing. Now imageGenStatus() returns the active model and
the composer defaults its picker to it, matching the Active-models panel; the
fallback skips Core ML. FAST/QUALITY now behave (512/10 vs 1024/12 on a distilled
model like DreamShaper).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imagegen): remove redundant FAST/QUALITY toggle
Size and Steps are already exposed as their own controls, so a FAST/QUALITY tier
toggle that just sets those same two values was redundant and confusing (and made
non-distilled models show '28 steps in FAST'). Removed the toggle + the resolveTier
helper/tests. The model's sensible defaults (512/10 for distilled) still seed the
Size/Steps controls; the user adjusts them directly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imagegen): hide number-input spinner on the Steps field
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imagegen): route txt2img through one-shot sd-cli, drop resident server
The persistent sd-server held ~4GB of image weights resident alongside the ~5GB
chat model -> memory contention on 16GB machines -> laptop hangs + corrupted
(near-blank) output, and it was never verified end-to-end in-app. Route all
full-checkpoint txt2img through the one-shot sd-cli path (same karras/defaults),
which frees the model on exit. Verified: DreamShaper 512/10/karras produces a
crisp image in ~29s with no resident memory pressure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(imagegen): RAM-aware image models — Light Q4 DreamShaper + Recommended badge + smart default
Offer both DreamShaper XL v2 Turbo quants as selectable models and make the
image experience RAM-aware:
- Catalog: add the Q4_K "Light" DreamShaper entry (distinct id + filename,
~2.8GB) alongside the full Q8 one; tag Q8 [Versatile,Fast], Q4
[Versatile,Fast,Light]. Distinct ids + filenames track independently through
download/listInstalled/active-tracking (active keys on id OR primary filename).
- 'Light' tag rendered in the model list (emerald-300 outline, distinct from the
emerald 'Fast' tag).
- "Recommended for you" badge (filled emerald) on the image model that best fits
this machine's RAM, sorted to the very top (above 'Fast'). Pure helper
recommendedImageModelId(models, ramGb) in @offgrid/models — keys off the
'Light' tag + kind==='image', not a model name; Light on <=16GB, full above.
- Smart RAM-aware default in resolveModel(): prefers DreamShaper and picks Q4 on
<=~17GB else Q8, only when the user hasn't set an explicit pick. Pure helper
defaultImageModelFilename() in src/main/image-default.ts.
- Rebuilt packages/models/dist (tsup) with the new exports.
Unit tests: recommend-image.test.ts (9) + image-default.test.ts (4), both under
src/main/__tests__ so the app's vitest gate runs them. Gates: tsc node+web clean,
182 tests pass, production build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(models): Light (Q4) variant for every image model + versatile-first recommend
Adds a Light (Q4_K) sibling for all 7 offgrid image models (realvis, realvis-
lightning, juggernaut, animagine, illustrious, pony — dreamshaper Q4 already
added), each ~35% less memory so it runs on a 16GB Mac. Every model is now
listed as both full (Q8) and Light (Q4), both selectable. recommendedImageModelId
now prefers the 'Versatile' all-rounder (DreamShaper) so the Recommended badge is
stable regardless of how many Light variants exist / their catalog order. Rebuilt
@offgrid/models dist. Tests: versatile-first pick + every-family-has-Light guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(models): 'Fast' only on the distilled Light (Q4) variants
'Fast' now means few-step AND memory-safe. Removed it from the full Q8 distilled
models (dreamshaper/realvis-lightning Q8) — they're few-step but ~4.2GB pegs a
16GB Mac, so advertising them as Fast is misleading. Kept off the non-distilled
Light models (juggernaut/animagine/etc Q4) — those are low-memory but still
28-step slow. Net: Fast = the Q4 distilled ones (dreamshaper-Q4, realvis-lightning-Q4).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(imagegen): add Light-model sample images + Storage tab download counts
Sample generations (512, app defaults) for the Light (Q4) models — dreamshaper,
realvis, realvis-lightning, juggernaut, illustrious — verified crisp, for the PR
showcase. Also: Storage tab label now shows live installed count + downloading/
failed badges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dictation): use native audio rate, not forced 16kHz (fixes stretched/garbled recordings)
Forcing new AudioContext({ sampleRate: 16000 }) made ctx.sampleRate report 16000
while the worklet was fed native-rate (44.1kHz) frames in this Electron build, so
PCM got mislabeled 16k: audio played ~2.75x too slow (14s clip showed 39s) and
transcription heard garbled slow-motion -> 'No speech detected'. Use the native
context rate reported honestly; the main process already resamples to 16k via
ffmpeg before transcription (alreadyWav16k=false when rate != 16000).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dictation): resample to 16kHz IN the worklet — fixes stretched audio, keeps live transcription
Root cause: the recorder streamed raw PCM labeled with ctx.sampleRate, but on
headphones (48/88.2kHz) the reported rate didn't match the delivered samples, so
recordings were saved at the wrong rate -> 2-3x time-stretch -> garbled -> 'No
speech'. Instead of trusting the (lying) context rate, the AudioWorklet now
downsamples every frame to a FIXED 16kHz using its authoritative global sampleRate
(linear interp, fractional position carried across blocks). We always stream +
label 16kHz, so it's immune to any device rate — and LIVE interim transcription is
preserved (still streaming PCM, just correct-rate at the source). Verified the
resampler preserves duration exactly (48k->16k = 3:1, no stretch).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dictation): revert in-worklet resampling; capture native + ffmpeg-resample
The in-worklet 16k downsampler had no anti-aliasing filter, which degraded the
audio (harsh/muffled) and wrecked ASR (sparse/garbled transcripts) even though the
duration was correct. Revert to native-rate passthrough + honest ctx.sampleRate;
main resamples to 16k with ffmpeg's proper anti-alias filter (the path that
transcribes cleanly). Saved WAV stays native rate (correct duration + full quality).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(transcription): decode 16k PCM WAV to Float32 (decodeWavPcm16)
Lets the dictation VAD/speech-ratio gate run on ffmpeg-decoded audio without a
second pass. Round-trip + edge-case tests (header-only, odd trailing byte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dictation): capture with MediaRecorder, ship blob + interim to main
Renderer records via MediaRecorder (correct rate by construction) instead of a
raw-PCM AudioWorklet. On stop it ships the full webm/opus blob for the final
transcript; between 1s timeslices it ships the growing recording for live
interim. AudioContext now drives only the level meter (AnalyserNode).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(modality-queue): priority lock + memory-admission (Phase 0)
A single owner that serializes heavy model work by priority so two big models
never sit resident on 16GB unified memory (the swap/beachball fix). Pure policy
(tiers, FIFO, tier-3-yields, tier-1-coexists) is unit-tested; the async lock +
model-eviction hooks are the thin IO layer. Engines self-register as evictable;
the queue never imports them. 21 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(imagegen): route image gen through the ModalityQueue (evicts LLM)
Replaces the ad-hoc inline llm.pause()/resume() bracketing with
modalityQueue.run({ tier: 2, evicts: ['llm'] }, ...). The LLM registers itself
as evictable at startup; warm-back happens once in the generateImage wrapper.
Behavior-preserving with the queue enabled (default).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(transcription): resident whisper-server engine behind the seam
A resident STT server (model stays warm) so live-interim ticks don't reload the
model each call. Added as 'whisper-resident' in select.ts, degrading to one-shot
whisper-cli when the binary isn't built (same pattern as Parakeet). Includes
build-whisper.sh mirroring build-llama.sh's gates (minos/rpath/foreign-dep).
NOT yet verified working: the binary needs a real CI build + on-device model-load
check before use (see the agent's remaining-to-verify list). Pure helpers tested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(runtime): residency contract (dynamic vs resident) per modality
Shared foundation for per-engine on-demand/in-memory mode: settings key +
typed accessor + pure normalize (defaults match today's behavior so nothing
changes until toggled). Engines read getResidencyMode('llm'|'image'|'stt'|'tts')
to decide whether to stay warm (queue evicts on contention + re-warms) or free
themselves after each job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(llm): dynamic vs resident mode via queue evict + mode-aware re-warm
The ModalityQueue now re-warms an evicted engine when its job finishes, and the
re-warm is mode-aware (per runtime-residency): resident reloads immediately (warm
next chat); on-demand just releases the pause block (llm.releasePause()) so the
server stays down and lazily respawns on next use, freeing its RAM meanwhile.
Correctness: the queue no longer tracks exact residency — it ALWAYS evicts a
declared id (evict is idempotent when down), so an on-demand engine that lazily
reloaded can't slip through and leave two models resident (the beachball this
whole system prevents). Image gen's inline llm.resume() is removed (queue owns it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(runtime): single ManagedRuntime seam for residency (SOLID/DRY)
One place turns residency mode into behavior (runtime-manager.registerRuntime +
warmActionForMode); every engine implements the same ManagedRuntime interface and
registers identically. If residency works for one engine it works for all; if it
breaks, all break — no per-engine drift. LLM is the first onto the seam (llm.runtime),
replacing its bespoke queue closure in index.ts. Mode-reader is injectable so the
wiring is tested against the REAL queue with a REAL runtime (no logic mocks).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(tts): dynamic vs resident via the shared ManagedRuntime seam
Adds a persistent 'serve' worker mode (loads Kokoro once, answers many synth
requests over NDJSON stdin/stdout) for resident mode; on-demand keeps the
one-shot per-call worker. synthesize() routes by getResidencyMode('tts'); tts
joins the queue through registerRuntime(ttsRuntime) exactly like the LLM — evict
kills the warm worker, warm preloads it, release ensures none lingers.
NOT yet verified on-device (Kokoro audio + persistent-worker IPC need a real run);
typechecks clean, worker syntax checked, existing tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(image): dynamic vs resident via the shared ManagedRuntime seam
Resident mode re-enables the warm sd-server fast path (~45s cold -> ~7s warm) for
eligible full-checkpoint txt2img (no LoRA/init-image/Z-Image/CoreML). It was
removed before for 16GB memory contention; now SAFE because the queue evicts the
LLM before image gen AND image registers as a ManagedRuntime (evict stops the
server so another modality can reclaim RAM). Default stays on-demand = the exact
one-shot sd-cli path as today, so nothing changes unless the user opts in.
NOT yet verified on-device (resident sd-server output quality on 16GB is the exact
thing flagged when it was removed); typechecks + build + tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(stt): dynamic vs resident via the shared ManagedRuntime seam
STT joins the seam like every other engine. residentAwareEngine() maps the chosen
dictation engine + STT residency mode to the engine to request: resident routes
whisper to the warm whisper-server (degrading to one-shot whisper-cli when the
binary isn't built), on-demand uses whisper-cli; Parakeet stays one-shot CLI in
both modes (no resident server — honest, no false 'resident'). The controller
resolves this once (sttEngine()) for all three transcribe sites. sttRuntime.evict
stops the whisper-server so another modality can reclaim RAM.
Resident STT only truly stays warm once whisper-server is CI-built + verified
(tracked separately); until then it degrades to on-demand. Typechecks+build+tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(settings): per-engine residency UI + residency integration tests
Adds a core 'Model memory' Settings section with an in-memory/on-demand toggle per
engine (chat, image, dictation, TTS), wired through runtime:residency:get/set IPC +
preload. Backend already reads these (runtime-residency); this lets the user set them.
Tests (real objects, no logic mocks):
- residency-integration.test.ts drives the REAL ModalityQueue + REAL registerRuntime
seam with real in-memory engines: asserts resident reloads / on-demand stays down /
runtime mode-flip / the always-evict correctness guard / strict heavy-job serialization.
- Verified the resident TTS 'serve' worker end-to-end out-of-band (real subprocess,
real NDJSON, real WAV) — protocol confirmed.
469 tests pass, 3 typechecks + production build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(models): register free-form HuggingFace downloads as installed
MiniCPM (and any HF search-bar model) downloaded fine but landed in 'unused files'
and never became a usable option: downloadModel streamed files to disk but, unlike
importLocalModel, never RECORDED the model, so listInstalled + the orphan sweep
never knew about it (and the card reset to Download on completion).
Fix: a downloaded-models registry keyed by the HF repo id (so the existing
'CATALOG.find(id) ?? resolveHuggingFaceModel(id)' lookups re-resolve it for
activate/delete with zero new branching). downloadModel records a non-catalog
download on success; listInstalled, getStorageInfo (known + models list), getCatalog,
and deleteModel all consult it; the ModelsScreen card drops the !isHf gate so a
downloaded HF model flips Download -> Use.
Verified against a real temp dir with real files (downloaded-models.test.ts, 7
tests) INCLUDING a reproduction of the pre-fix bug (unregistered files = not
installed + unprotected -> orphaned). Not yet run against a live HF fetch of the
specific minicpm5 repo (needs the app + network).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(residency): lock the chat model to in-memory (screen replay depends on it)
The chat LLM can no longer be set on-demand: the always-on screen-replay/capture
pipeline distills frames through it continuously, so on-demand would leave it
evicted after any image gen and thrash-reload the ~5GB model. Enforced in the
contract (normalizeResidency coerces + setResidencyMode ignores a locked modality,
so a stale/hand-edited on-demand can never take effect) AND in the UI (the Chat
model toggle is disabled, shows 'in-memory (required)', with a why tooltip). The
queue still evicts it MOMENTARILY during a competing heavy job then re-warms it -
that's contention handling, not an on-demand choice. Other engines stay togglable.
478 tests pass (added lock coverage; integration on-demand cases moved to an
unlocked modality since llm-on-demand is now correctly impossible), build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(queue): interactive chat runs as Tier 2 (outranks background replay)
Route the user-facing rag:chat generation turn through modalityQueue.run({tier:2,
label:'chat'}) so foreground chat registers as foreground and the Tier-3 background
screen-replay distill yields to it. Chat evicts nothing (it runs ON the 'llm' engine);
it's a peer of image gen (also tier 2) - during image gen the LLM is evicted so chat
waits anyway, which is consistent. The abort controller now registers BEFORE queuing
so a cancel while the turn is still waiting is honored. Internal/background llm.chat
calls (summaries, extraction, intent) stay unqueued.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(chat): always-visible Image toggle + 256 image size
- Image toggle chip now lives in the composer row next to Thinking (enabled by
default), so image mode no longer requires the + dropdown - clicking it forces
image generation for the next prompt; the × / re-click returns to chat.
- Add 256 to the image Size options (fast low-res draft; valid multiple-of-64
dimension, no downstream clamp). Rough on SDXL models, fine for quick drafts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(review): WAV data-chunk scan, chat evicts resident image, TTS lifecycle, em dash
Review findings addressed:
- decodeWavPcm16 scanned the RIFF chunk list for 'data' instead of assuming byte 44.
The bundled ffmpeg writes a LIST/INFO (encoder tag) chunk before 'data' (PCM starts
at byte 78), so the fixed-44 read fed the VAD/silence gate loud garbage and could
drop a real transcript. Regression test added for the LIST-chunk layout.
- Chat jobs now evict 'image': a resident image server was never freed before a chat
turn (nothing declared evicts:['image']), so a resident image + the LLM could coexist
on 16GB - the exact OOM the queue exists to prevent.
- Resident TTS worker gained idle-evict (5m) + onHostQuit + a per-request timeout;
it previously lived for the whole session (nothing evicts 'tts') and a hung worker
wedged 'busy' forever.
- Settings residency tooltip: em dash -> ' - ' (CLAUDE.md copy rule).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: User <user@Users-MacBook-Pro-174.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 6aee58a commit d908bc4
75 files changed
Lines changed: 4700 additions & 951 deletions
File tree
- .github/workflows
- docs
- e2e/screenshots/imagegen
- packages/models
- dist
- adapters
- src
- resources
- scripts
- src
- main
- __tests__
- modality-queue
- __tests__
- rag
- transcription
- __tests__
- preload
- renderer/src
- components
- lib
- __tests__
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
132 | 141 | | |
133 | 142 | | |
134 | 143 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
29 | 32 | | |
30 | 33 | | |
31 | 34 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
Loading
Loading
Loading
Loading
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| |||
0 commit comments