Spaces:
Runtime error
Runtime error
Commit History
-
Restore Docker app for Klein sidecar
0e4c895 -
Export server as Gradio demo
eec00bf -
Use Gradio server launch defaults
039254c -
Prioritize custom routes in ZeroGPU mode
987cfbf -
Attach custom routes to Blocks for ZeroGPU
b25f12e -
Let Spaces launch Gradio server mode
fc380b0 -
Run Tiny Army directly on Gradio ZeroGPU
fe58bfe -
Use compatible Gradio client token argument
103347c -
Proxy Klein portraits through ZeroGPU sidecar
4fd3bea -
Add ZeroGPU Klein portrait provider
844dab5 -
Portraits: in-browser WebGPU via vendored Bonsai (FLUX.2-Klein 4B, on-device)
Replaces the broken Janus engine with Bonsai Image 4B (FLUX.2-Klein, 1-bit/ternary, Apache-2.0) — the only in-browser WebGPU image model that actually works (verified SOTA-quality portraits on an RTX 3090). Bonsai's engine is a bespoke WGSL app with a custom 1-bit weight packing and no JS API, so we can't load its weights into a generic pipeline. Instead we VENDOR its self-contained static bundle (index.html + one 912 KB JS, Apache-2.0) under web/bonsai/ and run it in a same-origin hidden iframe, driving its UI directly (load → prompt → GENERATE → read the output <img> into a Blob via canvas). Same-origin is the key: it avoids the cross-origin background-throttling that hung earlier iframe attempts, and lets us drive the app's DOM directly (no postMessage bridge). Desktop-only (~4-5 GB model); shown disabled on mobile, where cloud FLUX serves instead. Verified end-to-end in the persona panel: load → paint → 512² portrait cached per hero. Co-Authored-By: Claude Opus 4.8 (1M context) …db6b273 -
Portraits step 4: in-browser Janus-Pro-1B (WebGPU) portrait engine
Adds web/imagenJanus.js — Janus-Pro-1B (DeepSeek, MIT) via Transformers.js + WebGPU, the SAME runtime our LLM/Kokoro engines use. Runs on the device (no cloud); generate_images → RawImage.toBlob() → PNG. WebGPU-required, fp16 weights where shader-f16 is available else fp32, prepare-inputs on WASM. Wired into the imagen.js engine list and the Settings Portrait picker (selectable alongside Z-Image-local + cloud FLUX). The persona panel now shows one-time download progress for in-browser engines before painting. Verified: appears + enabled in the picker. On-device generation needs a real WebGPU browser (not the SwiftShader headless one). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>69e8c08 -
Portraits step 3: persona panel Portrait section + per-hero caching
- New "Portrait" section in the hero panel: an editable appearance prompt (auto- built from name/class/about) and a 🎨 paint button that generates a portrait via the active image provider (local Z-Image or cloud FLUX), shown beneath it. - Reuses the voice spinner/badge UX (now generalized to .persona-ico): the button badges when there's no portrait or the appearance changed, spins while painting. - Per-hero PNG cached in IndexedDB (new 'portraits' store, DB v2) and the appearance prompt persisted, so portraits survive a refresh and live in the barracks. Verified locally: section renders with auto-appearance; 🎨 paints a 1024² portrait (~15 s on the 3090) that renders + persists across reload; badge clears when current. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>6a861b9 -
Portraits: set correct Content-Type for cloud images (NIM returns JPEG)
Sniff the image magic bytes so the response Content-Type matches the actual format (FLUX/NIM returns JPEG, local Z-Image returns PNG) — keeps the cached blob's type correct on the client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>acb2593 -
Portraits step 2: /portrait engine routing + imagen facade + Settings picker
- app.py /portrait now honors an `engine` hint ('local' | 'cloud' | auto), so the frontend can pick Z-Image-on-GPU vs cloud FLUX explicitly. Local loader uses 4-bit NF4 + a per-process VRAM cap (peak ~8 GB) so it can't crash the display. - web/imagen.js — image facade mirroring tts.js (engines, persisted activeId, onImageEngineChange, generatePortrait → PNG Blob). - web/imagenServer.js — engines backed by /portrait: zimage-local (your GPU, localhost-only) + flux / flux-dev (cloud NIM, HF fallback). - web/imagenBar.js + settingsPanel.js — a "Portrait" provider picker section, injected after Voice, persisted and synced like the voice picker. Verified locally: picker renders/persists; engine:'local' generates a 1024² PNG; engine:'cloud' cleanly 503s without keys (cloud verified in prod). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>2151ea1
-
Settings: hide the app URL printed in Gradio's Settings header
Gradio's Settings header rendered "Settings <app-url>" (tinyarmy.noods.cc in prod). Hide that .url div so the header just reads "Settings". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>476605d -
Settings: "Use recommended for this device" button (GPU/device-tier heuristic)
Inside the "Your device" panel, a button picks a preset from the hardware: - Mobile → Low, but Medium if a Qualcomm Adreno 700-series (or higher) is found. - Desktop → Medium, but High if an NVIDIA RTX 3090 (or higher) is found (newer generation, or 30-gen ≥90; so 3090/3090 Ti and all 40xx/50xx qualify). GPU is read from the WebGL ANGLE renderer string + WebGPU adapter info. Applies and persists the chosen preset, and shows what it picked and why. Heuristic unit-tested across 13 GPU strings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>7b49a92 -
Settings presets: real padding (beat Gradio reset) + readable active hover
Gradio's button reset was zeroing the preset buttons' padding, so the text sat against the edges — restore it with !important (14px 16px), like the other persona buttons. Also: a selected button turns light on hover, so force its text to ink (dark) on hover instead of staying light-on-light. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>dbd7553 -
Settings: recommended quality presets + device readout; persist all choices
New "Recommended settings" section at the top of Settings (above Local AI Model): - High / Medium / Low presets that set the LLM model AND voice provider together, like a game's graphics presets: High = Qwen3 0.6B + Kokoro 82M Medium = Qwen2.5 0.5B + Kitten TTS Low = Qwen2.5 0.5B + Web Speech Presets resolve the model within the active engine's catalog (family fallback). Changing the model or voice by hand drops to Custom. - Expandable "Your device" readout (CPU threads, RAM, WebGPU/GPU, storage, screen, UA) for debugging and benchmarking. Persistence: the LLM engine + per-engine model (runtime.js), the voice provider (tts.js), and the preset choice (qualityPreset.js) are all saved to localStorage, so settings survive a refresh. runtime.js now validates a stored model against the active engine and emits onModelChange; the model/voice bars re-sync when a preset changes them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>750ca83 -
Voice: default prod provider to Kokoro (in-browser), not cloud Qwen3-TTS
Prod now defaults to Kokoro — runs on the device with no exhaustible cloud quota. Localhost still defaults to local-GPU Qwen3-TTS; cloud Qwen3-TTS stays selectable in Settings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>585578b -
Voice: provider-driven per-hero voices; settings is provider-only
Settings → Voice is now just a provider picker (no global voice dropdown, no "narrate war diaries" checkbox) — the voice belongs to the hero, not the app. Copy updated to explain the model. Persona panel now respects the chosen provider: - Qwen3-TTS (design): voice-design text stays editable; designs/clones as before. - Kokoro/Kitten/Web Speech (fixed-voice): the design text goes read-only and a per-hero "Voice" dropdown of the provider's named voices appears. Play synthesizes the quote in that voice (cached as a WAV for Kokoro/Kitten via a new PCM→WAV encoder; spoken live for Web Speech). The pick is saved per hero (voiceId) and the cache tracks voiceIdUsed for staleness, mirroring Qwen3. The provider is set on another tab, so tts.js now emits onTtsEngineChange and the panel re-renders its voice controls live (no reliance on tab visibility). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>e648dca -
Persona: simplify prompt to name/about/quote/voice; editable in Settings
- Drop the specialty/personality/vibe tags from the generation prompt and the hero panel — only name, about, quote and voice design remain. - New Settings → "Persona Prompt" section: view and edit the system prompt that writes each hero. Saved locally (tinyarmy.personaPrompt) and used on the next "Recruit hero"; Reset restores the built-in default. - personaPrompts.js exposes PERSONA_SYSTEM_DEFAULT + get/set/reset/isCustom; personaPanel.js calls getPersonaSystem() so an override wins. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>575fb61 -
Persona voice: re-DESIGN when the voice description changes (not clone)
Editing the voice design now regenerates a fresh timbre instead of cloning the old voice. play() cloned whenever a cached voice existed, so a changed design just re-spoke the old voice. Now it clones only when the timbre should be kept (voice unchanged, new words) and designs otherwise (new/edited description). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>c531198 -
Persona voice: play↔stop toggle, loading spinner, auto-stop on nav/new pick
- ▶ becomes ⏹ while a voice is sounding; clicking again stops it. - While a voice is being generated the button shows a spinning loading ring (CSS .busy::before) — the spinner clears the instant the WAV is ready, then the button flips to ⏹ for playback. - Voice auto-stops when: navigating away from the Personas tab (Intersection Observer on the hidden stage), picking another hero from the barracks, and starting a new generation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>ba238a8 -
Persona voice: cloud voice-clone, varied voice designs, animated class picker
- app.py: DashScope cloud voice-clone for prod — two-call enroll (qwen-voice-enrollment) → synthesize (qwen3-tts-vc-2026-01-22), then fetch the returned OSS audio URL. Wired into /qwen-tts when ref_audio is present (mirrors the local design→clone flow). Verified flow returns 200 + audio when the account's paid tier is enabled. - personaPrompts.js: voice field now asks for a DISTINCT, class-fit voice with wide variety between heroes (was producing near-identical gruff voices). - personaPanel.js/persona.css: class picker is now a custom dropdown showing each class's looping idle sprite (front-right row, Web Animations API), wired to characters.json; mirrors a native <select> (.value, 'change'). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>26cb5c0 -
Personas → heroes: shield icon, hero terminology, empty state, active highlight
- Nav icon for Personas: 🪖 (contemporary helmet) → 🛡 (fantasy shield). - "Recruit a soldier" → "Recruit hero"; all "soldier" copy → "hero" ("No heroes saved yet", prompts, store defaults, diary). - Persona page hides the hero fields until a hero is generated or picked (empty state "Recruit a hero, or pick one from the barracks"). - The selected hero is highlighted in the barracks roster (ink fill + red accent bar). Verified: icon, empty/shown toggle, active highlight, terminology. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>29474fc -
Fix: red headings, voice-edit badge, and voices persisting across refresh
- Persist voiceQuote + voiceDesignUsed in the saved persona record. Without them, a reloaded persona always read as "dirty" so ▶ re-synthesized instead of replaying the cached file — that's why "voices weren't saved on refresh." Now a saved voice reloads as current and replays from IndexedDB; only real quote/voice edits mark it stale. - Section headings: color !important so Gradio's text var can't blacken them (now red). Verified red = rgb(216,39,26). - Confirmed: editing the voice design (not just the quote) badges ▶; audio + voice-state survive a page reload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>9f5e779 -
Persona voice: one pulsing play button, sidebar-style headings, live badge
- One ▶ play button (create button removed). Always shown on the Quote heading; it designs the voice when there's none, clones the last voice when the quote/voice was edited (same timbre), or replays the cached file when current. - Pulsing red badge on ▶ whenever there's no voice yet OR the quote/voice changed — updates LIVE as you type (input listener), not just on blur. - Section headings restyled like the sidebar: a short ink line before a small red uppercase heading; the action button is anchored right after the heading (not at the page edge). Verified: badge pulses on a no-voice persona; headings + button placement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>ea860c1 -
Persona: red section headings, icon buttons, edit-aware voice + clone-on-play
- Result laid out in sections (line + red heading): About, Quote, Voice design. - ▶ play icon sits on the Quote heading; 🎙 create/recreate sits on the Voice design heading; both simplified to icons (play is neutral, not red). - Editing the quote or voice marks the cached audio stale → a red badge on ▶. - Tapping ▶ when badged does NOT redesign the voice: it CLONES the last voice file (Qwen3-TTS Base model, keeping the exact timbre) for the new line, plays it, saves over the old file, and clears the badge. Otherwise ▶ replays the cached file. 🎙 is the explicit "new voice from the description" path. - Backend: TINY_TTS_MODE=local loads the Base model lazily and clones via generate_voice_clone((numpy,sr) ref). Prod (DashScope) gets the voice description so it re-designs gracefully (no clone model). Verified clone end-to-end on the GPU (5s). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>72160ec -
Persona: editable fields (auto-save), quote after about, Create/Replay voice
- Reorder result: name → tags → about → quote → voice design. - All fields (name/about/quote/voice) are click-to-edit (contentEditable); every edit auto-saves to the roster — no Save button. Generated personas auto-save immediately. - "Hear voice" → "🎙 Create voice": synthesizes the QUOTE in the designed voice, caches the WAV in IndexedDB (personaStore put/getAudio), and plays it. Once created, a ▶ replay button beside the quote replays the exact cached file; button becomes "Recreate voice". - tts.js: createVoiceWav() returns the raw WAV (caller caches/plays); qwen3.synthWav() added. Verified end-to-end on the local GPU: synth → cache → retrieve. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>308478f -
Personas-as-agents: spoken quote + save to a local-first barracks roster
Each persona now also gets a `quote` (a short spoken line). "Hear voice" designs the voice and speaks the QUOTE (not the bio). New personaStore.js — a local-first roster (localStorage JSON blob + CRUD + change listeners + a pluggable sync hook), modeled on woid's Shelter store — so saved soldiers persist for returning visitors. Persona panel gains "💾 Save to barracks" + a roster list (view / remove); MAX_TOKENS 160→200 for the extra fields. Verified: save → reload → persists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>0ecdf98 -
Voice: cloud Qwen3-TTS is the default engine in prod (was Kokoro)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>fc20b4d -
Voice settings: add "Qwen3-TTS local" engine — localhost-only, default on localhost
- New TTS engine 'qwen3local' (Qwen3-TTS · local (your GPU)) that POSTs to same-origin /qwen-tts. available() = isLocalhost(), so it's enabled on localhost and shown DISABLED with a "run the project locally" note in prod. - Default TTS engine is now qwen3local on localhost, Kokoro in prod (local-first). - ttsBar shows each disabled engine's note instead of "n/a". - app.py: TINY_TTS_MODE=local runs the open weights in-process (lazy qwen-tts, GPU), so the local engine's same-origin /qwen-tts serves the model — the LeLab pattern (one origin, no CORS/cert). Unset on the Space → DashScope as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>e352ff3 -
app.py: PORT env (default 7860) so it runs locally next to a service on 7860
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>0f47058 -
Add Qwen3-TTS local bridge: ?tts= override + tts_server.py
LeLab-style: point the (hosted or local) UI at a self-run Qwen3-TTS server so voices are designed on YOUR GPU, off the grid. - ttsQwen3.js: endpoint is configurable via a ?tts=<base> query param (persisted to localStorage; ?tts= clears it). Default stays the same-origin /qwen-tts (DashScope). backendLabel shows 🖥 host when bridged. - tts_server.py: standalone FastAPI server running the open weights (Qwen3-TTS-12Hz-1.7B-VoiceDesign) via the qwen-tts package, POST /qwen-tts {text,instruct,language} → WAV, CORS-open. QWEN_TTS_STUB=1 returns a tone so the bridge can be smoke-tested without a GPU. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>3bc1b31 -
Move Voice settings into the Settings page; add a sidebar ⚙ Settings button
- settingsPanel.js injects two sections into Gradio's settings page now: "Local AI Model" + "Voice" (the TTS engine/voice picker, moved off the diary page). Both ride the shared singletons. Auto-narrate is now global facade state (getAutoNarrate / setAutoNarrate) so the diary reads it without the bar being mounted. - diaryPanel: drop the voice bar; keep the Read-aloud button + narration; read getAutoNarrate() for "narrate as it writes". - Sidebar gains a "⚙ Settings" item (nav.json) that opens the SAME Gradio settings page as the footer link (tiny.js wraps tacNavigate to click the footer button). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>dffe06d -
Qwen3-TTS: use DashScope international endpoint (our key is sk-ws- intl)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>dff270e -
Add Qwen3-TTS Voice Design (DashScope) + persona voice descriptions
Reuse the oikomi Space's one trick — proxy Alibaba DashScope's qwen-voice-design API — but with our own DASHSCOPE_API_KEY (Space secret), server-side so the key never hits the browser. No GPU/ZeroGPU needed. - app.py: POST /qwen-tts {text, instruct} → DashScope qwen-voice-design → 24kHz WAV. - web/ttsQwen3.js: new TTS engine (mode pcm) calling /qwen-tts; the "voice" is a free-form description (preset, or the persona's). Registered in the TTS facade with setVoiceDescription() + previewVoice(); labeled cloud (not local-first). - Persona generation now emits a `voice` field (one-line how-they-sound description); cap raised 100→160. Parser keeps it; panel shows it + a 🔊 Hear voice button that designs the voice and reads the about aloud. - Diary: Qwen3 "persona voice" is designed from the unit's traits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>717332c -
Cap persona + diary at 100 tokens; shorten prompts; stream the diary
- maxTokens 100 for both persona and diary (was 220/768 via thinkMaxTokens) so models can't ramble past the answer. Prompts tightened to match: persona about → "1-2 short sentences (~25 words)"; diary → "1-2 vivid sentences (~60 words), be brief". - War diary now streams visibly: it used the aggressive stripThink live, which an unclosed <think> emptied — so it showed blank until the end. Use stripThinkFinal in the live onToken so the prose (and narration) appear token-by-token. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>b862211 -
Fix empty-JSON: keep the answer when a model leaves <think> unclosed
Root cause (from a real debug report): Qwen3 on WebLLM opens a <think> tag and jumps straight to the answer WITHOUT closing it. Our stripThink had a rule that deletes an unterminated <think> all the way to end-of-string — so it nuked the entire JSON answer and the parser saw 0 chars ("model returned empty / unparseable JSON"). The model was actually producing a perfect, fully-filled persona. Add stripThinkFinal() (drop closed <think>…</think> blocks + stray unclosed tags, KEEP the answer) and use it for the final persona parse + diary display; keep the aggressive stripThink only for the live streaming preview. Verified against the exact failing output: parses name/about/specialty/personality/vibe correctly. Also: settings section now inserts directly above "Display Theme" (below the Settings title), not at the very top of the page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>62070d0 -
Model bar: show params once (avoid '0.6B · 0.6B' for byte-less engines)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>5264fdb -
Settings: model section to top; default WebLLM + Qwen3 0.6B; copyable debug
- Inject the "Local AI Model" section at the TOP of Gradio's settings page. - Default engine = WebLLM (best on mobile WebGPU; falls back to wllama with no WebGPU) and default model = Qwen3 0.6B. - Persona + diary panels gain a "📋 Copy debug" button that copies a paste-ready report (engine · backend · model · input · outcome · raw output · stripped text, plus exception/stack), so failures like the empty-JSON persona can be sent back verbatim. Clipboard-blocked contexts fall back to selecting the text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>898540a -
Put the model picker in Gradio's OWN settings page (not a custom tab)
Per the clarification: inject the engine/model picker into Gradio's native settings page (footer "Settings" → ?view=settings) instead of a custom sidebar tab. Gradio's settings page isn't an official extension point (footer_links only toggles which of api/gradio/settings show), so settingsPanel.js anchors on its "Display Theme" section, clones the section + heading classes, and appends a native-looking "Local AI Model" section driving the shared runtime.js singleton. Reverts the custom Settings tab + nav item + #settings-stage. Graceful no-op if Gradio's DOM changes (app still runs on the default model). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>cd43499 -
Move engine/model settings to a Settings page; share one model across both pages
The model picker was duplicated on Personas and War Diaries. Move the engine + model selector and cache controls into a new Settings tab (settingsPanel.js); both pages now read the same selection from the runtime.js singleton, so there's one place to choose the model and it's guaranteed identical on both. The diary page keeps its Voice (TTS) controls. nav.json gains an "App › Settings" item (Space-only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>627d835 -
Cache status + delete for WebLLM and Transformers.js (like wllama)
The model bar already shows "✓ downloaded" + a delete button for any engine that exposes cachedSet()/deleteCached(); wire those up for the other two: - WebLLM: MLC's own hasModelInCache() / deleteModelAllInfoInCache() over prebuiltAppConfig (matches the q4f16/q4f32 variant we'd load). - Transformers.js: inspect the 'transformers-cache' Cache API store; only count a model as cached when its *.onnx weights are present (Transformers.js sometimes caches the config/tokenizer JSONs but not the big model file — known upstream issue), so the badge is honest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>b0f48f8 -
Transformers.js: pre-flight WebGPU device, else WASM (fixes load + caching)
Verified headlessly: Transformers.js DOES cache (writes a transformers-cache store) — the real problem was it never loaded. navigator.gpu can exist and requestAdapter() succeed, yet the WebGPU backend still throws "no available backend", and a failed WebGPU attempt poisons the in-context WASM retry. Now we pick the device up front via requestDevice() and only use WebGPU when a real device is obtained; otherwise WASM from the start (which loads and caches reliably). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>bed3298 -
Fix stale JS (no-cache on /web) + Transformers.js WebGPU→WASM fallback
- "stops at 220": the browser was serving a stale cached personaPanel.js (old token cap) because /web assets had no Cache-Control. Set Cache-Control: no-cache so modules always revalidate (cheap 304 when unchanged). Model weights come from huggingface.co, so this doesn't touch their caching. - Transformers.js picked device:webgpu whenever navigator.gpu existed, but if the adapter request fails (headless / flaky drivers) it threw "no available backend" instead of falling back — so the model never loaded and never cached. Catch and retry on WASM; backendLabel now reflects the device actually used. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>559041f -
Fix Qwen3 on WebLLM: budget for thinking + capture reasoning_content
Symptom: WebLLM Qwen3 "rambled" but the output was just unfinished. MLC doesn't honour the `/no_think` text switch (wllama does), so Qwen3 keeps reasoning, and max_tokens:220 caps the TOTAL (reasoning + answer) — the JSON gets truncated before it closes, so parsing fails. - thinkMaxTokens(): give thinking models (qwen3) ≥768 tokens so they can reason AND still finish the answer. When /no_think IS honoured (wllama/transformers) the model stops early at EOS, so this costs nothing there. - WebLLM stream now captures MLC's separate `reasoning_content`, re-wrapped as <think>…</think>, so stripThink + the raw "thinking" view treat every engine the same (and the budget math is visible). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>eba5aae -
Fix Qwen3 parsing (/no_think) + WebLLM double-load guard & progress clarity
Qwen3 is a thinking model: with a 220-token budget it spent everything on a <think> block and never emitted the JSON/answer, so persona parsing failed after the thinking. Append Qwen3's `/no_think` soft switch for these structured tasks (persona + diary) so it goes straight to the answer. Helper noThink(modelId) keyed on the model id; no-op for non-Qwen3. WebLLM "downloads twice / no cache": - ensure() guarded on _loadedId (only set AFTER load), so a re-entrant ensure during a slow download could start a SECOND load. Guard on _loadingId (set up front) instead — same fix applied to the Transformers.js engine. - MLC runs two phases through one progress callback (Fetching from network, then Loading from cache into GPU) — both 0→100%, which reads as "downloading twice". Pass MLC's progress text through so the status line shows which phase it is; the second pass is a cache-load, not a re-download. (Caching = MLC Cache API + our storage.persist(); evictable under quota pressure.…ab87288 -
Mobile: collapse model/voice control bars behind tap-to-expand
On phones the diary had two full control bars (model + voice) stacking ~830px of controls above the story. Wrap each bar in a <details> that's open on desktop (summary hidden) and collapsed on mobile (tap "⚙ Model" / "🔊 Voice" to expand), so the war story / persona sits near the top on a phone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>0218dca