- Engine: when the primary summarizer is out of retries (or misconfigured),
run_summarize now finishes the job on the configured rescue provider
(SHONAR_LLM_FALLBACK_*), tags the summary with the provider that wrote it,
and stores a human note in job.error; success clears stale notes.
- App (auto/lan): passes Ollama as the rescue provider when it is up.
- Detail screen: shows the rescue-swap note in plain words, a red
'Summary failed' line with Settings -> Summarizer fix instructions and a
Retry summary button on hard failure.
- Settings copy explains the fallback. 3 new pytest cases (14/14 pass);
live E2E on 2026-09-18: sarcastic summary v6 via LAN on attempt 2.
- Detail: pinned header (leave from anywhere), Prev/Next + Alt+arrows + Esc
- Summary above transcript with full sections (key points/decisions/action
items/details) + Copy summary / Copy action items buttons
- Voice chips moved under the summary; one-page scroll below the header
- Stuck-at-99% fixed: detail re-fetches transcript+summary when a watched
job finishes while the screen is open
- Playback: Pause/Resume + seek slider + position readout over mpv IPC
(socket verified; controls hide if IPC unavailable)
- Library: sort Name/Newest/Oldest (persisted, date shown per row) and
status filter All/Untranscribed/Done/Failed
- Delete is undoable: trash + 10s Undo toast replaces confirm dialogs
(single + bulk); startup sweeps stale trash; engine purge on undo-window
close
- Progress bar + 'Summarizing… · <voice> (attempt n/3)' right under the
Re-summarize button: the top spinner hides behind a long report, which
made a running job look like nothing happened.
- Voices: + Enthusiastic, + Storyteller, + 'Other…' free-text (any
phrase, 60 chars; dialog shows how it reads in the prompt).
- Verified end-to-end on BOTS: sarcastic summarize succeeded, summary
v2 stored with tone=sarcastic ('…a heroic inability to monetize
anything'). BUILD SUCCESSFUL + 39 tests.
Chips: Neutral / Sarcastic / Funny / Dry wit. The chosen voice rides
the reprocess call and the button echoes it ('Re-summarize · Sarcastic');
an existing summary shows '· in a sarcastic voice' next to its heading.
compileKotlin + 39 unit tests green.
POST /reprocess?job=summarize&tone=<voice> persists the voice on the job
row (queue carries only ids, so a sweep re-enqueue keeps it), the LLM
prompt appends 'write every field in a <tone> tone — the tone colors
the wording, never the facts', and the resulting summary records its
tone (SummaryOut.tone). Plain re-summarize clears a previous tone.
Migration tone0000000001 (summaries.tone, processing_jobs.tone).
78 passed, 1 skipped; ruff clean.
Model cards + folder row overflowed in short windows (split-screen) with
no scroll path — the only screen where content was truly unreachable.
Library/Detail already scroll via LazyColumns; verified compileKotlin +
39 unit tests green.
Nothing read it; services/search.py already picks SQLite vs Postgres by
dialect. The 'postgres_fts' default was a leftover from the server-era
config and only invited confusion.
Every row had a Delete button that duplicated the bulk bar (check a
box -> Delete N). Removed it plus its now-dead single-file confirm
dialog; deletion is checkbox + bar from now on. Detail screen keeps
its own delete.
shared/ carried the vendored phone app's tree behind a 30-entry
exclude list. Deleted the 46 phone-only files (recording service,
widgets, Room DB, Nextcloud/local-only/folder-sync providers, TOFU
trust, Android settings stores, phone UI screens) and kept only the
seven the desktop compiles: CustomShonarProvider, ProviderTypes,
ShonarProvider, ShonarAuthStore, ShonarHandshake, ProviderRegistry,
AiContent. ProviderRegistry reduces to its id constant; handshake
drops the TOFU param; provider defaults self-identify as desktop.
Gradle exclude list gone — srcDir('../shared') is pure desktop now.
inFlight conflated queued with actually-running, so a file waiting in
the pump refused deletes ('is transcribing' — false) and hid its
Rename/Delete buttons during a batch. Track the truly-running files in
a dedicated set: the pump registers a file at poll-time atomically with
the queue pop, and deleteFile/renameFile either find it running (refuse)
or cancel its queue entry under the same lock (proceed). Batch progress
shrinks when a queued file is cancelled (batchRemove) so the bar still
ends; row status now distinguishes QUEUED from RUNNING on rescan.
Library header keeps only wordmark/Settings plus the auto-transcribe
and Transcribe-all controls. Settings gains a 'Recordings folder'
section (full path + Change folder…) at the top; the empty library now
shows a card that jumps straight to Settings.
Toolbar gets 'Transcribe all (N)' when N files still lack a transcript
(includes previously-failed ones — asking again means retry). Queues
them through the existing pump; hidden while a batch runs or when the
library is fully transcribed.
While a batch runs, a card shows 'Transcribing batch: k of N — <current
file>' with a bar that blends the settled-file count with the current
file's own backend-reported fraction. Disconnected mid-batch clears the
bar instead of stranding it.
Pure logic in LibraryQueue (transcribeAllPlan, batchAdvance) + 3 new
tests; app suite 37/37. Verified button renders on live build 07:34.
Registry now lists exactly two models instead of five:
- 'Whisper Base' — names the actual bundled model (was 'Base (default)').
- 'Whisper Large v3' — kept; honest copy on size/speed/RAM.
tiny/small/medium removed from the registry (validate_model_name now
rejects them; existing rows keep their saved model — all current data
is 'base', still valid).
Detail screen shows the model that WILL run by real display name
('Whisper Base (default)') instead of 'Use default (base)'; the picker
lists the two models with the default marked inline.
Backend 78 passed/1 skipped; app 34/34; engine hot-reloaded clean.
- setup-standalone.sh: falls back to system python3 -m venv + pip when
uv isn't installed (Debian/Ubuntu path; verified on clean bookworm).
- dist/Dockerfile.debian: debian:bookworm build env for :app:packageDeb
(needs fakeroot + /usr/share/desktop-directories — both baked in; the
dir gap made jpackage's postinst fail with exit 3 on minimal systems).
- dist/make-release.sh: clean source tarball from HEAD.
- INSTALL-debian.md: end-to-end install for friends (source tree must
stay on disk — the app spawns the engine from it via SHONAR_REPO).
Verified on real Debian 12 in Docker: :app:packageDeb builds
shonar-desktop_0.1.0-1_amd64.deb, apt installs it, setup-standalone.sh
provisions the engine (venv+pip path, model fetch, boot smoke test),
and the packaged binary launches under Xvfb.
Two compounding bugs:
1. Nodes skipped in a plain recomposition kept their old captured
density, so changing the zoom state left the tree unscaled — wrap the
CompositionLocalProvider in key(zoom) to force full-tree recompose.
2. Compose focus routing missed the shortcut depending on which child
held focus — add an AWT KeyboardFocusManager dispatcher below Compose
(also accepts Ctrl+Up/Down, numpad +/-, Ctrl+0 reset).
User-confirmed zooming works on the live app.
JFileChooser rendered unusably small on this HiDPI/Wayland compositor and
had no resize grip. Replace it with a Compose dialog matching the app's
navy+teal skin: breadcrumb row (Home + path steps), scrollable list of
subfolders as cards (dot-dirs hidden), '..' row, and a confirm button
naming the selected folder. Scales with the app's Ctrl+-/= zoom since it
lives inside the Compose tree. Pure nav logic in FolderPicker with 3 new
tests (34 total green).
- openDetail attaches to a pump-owned file with no mapping yet: mirrors the
pump's live stage/progress until it leaves in-flight, then picks up the
mapping/report — opening a mid-transcribe file now shows movement instead
of an idle Transcribe button.
- Detail screen gets a Play/Stop row and a 'Search this recording' box.
Searchable lines come from live segments or by parsing the [HH:MM:SS]
prefixes out of the saved report (round-trip tested); each hit card shows
its timestamp and plays the audio from that moment on click.
- Playback via mpv (external process; Java can't decode m4a) — exact flag
set verified on this box against a real recording. Stop on leave-screen,
on file delete, and at app shutdown; natural end clears the playing
state. 31/31 tests green.
Checkbox on every row; selecting any reveals a toolbar (N selected,
Select all/Clear all, Clear, red Delete N). One confirmation dialog names
every file (first 8 + count) before anything goes. deleteFiles() re-checks
in-flight state at confirm time and skips anything the pump grabbed since
selection (named in the notice) instead of yanking a live transcribe.
Selection keys on absolute paths and prunes vanished files on rescan so
the count never lies. Selection logic pure + unit-tested (28/28).
Library rows and the detail header get a red Delete action that removes the
audio, its .transcript.md report, and its .shonar.json mapping, then purges
the engine's copy in the background (DELETE ?purge=true). Refuses while a
file is queued/transcribing so the pump can't resurrect it mid-flight; a
failed background purge surfaces as an inline warning instead of blocking
the local delete (stale mappings that 404 are silently fine). Confirmation
dialog on both entry points states exactly what goes. deleteSet() is pure
and unit-tested (25/25 green).
- models/hf-cache/ vendors Systran/faster-whisper-base (142MB, gitignored);
engine spawn pins HF_HUB_CACHE there, so transcription works with a cold
~/.cache and fully offline
- verified: HOME=/tmp/fakehome HF_HUB_OFFLINE=1 model loads and transcribes
via the bundled cache alone
- setup-standalone.sh: idempotent fresh-clone provisioning (local venv,
model copy-or-download, engine boot smoke on :8123) — runs green
- backend/README rewritten for the desktop bundled-lite reality (was
pointing at a nonexistent root README/docs and a Postgres quickstart)
Kotlin suite still green (gradlew test, 0 failures)
- conftest: default SHONAR_TEST_DATABASE_URL is a temp SQLite file, matching
the bundled-lite engine; set the env var to a PG URL to exercise that path
- models.py: add sqlite_where to the two partial unique indexes — without it
SQLite built a FULL unique index on recording_id (WHERE not carried over),
wrongly blocking a second export asset per recording
- test_m9: pass UUID objects (not str) to direct ORM inserts/gets; SQLite's
GUID bind processor rejects strings (asyncpg tolerated them)
Verified: pytest -q = 78 passed, 1 skipped; ruff check clean
- backend/.venv was a symlink into ~/Projects/Shonar; replaced with a real
local venv built fully offline from the uv cache (deps pinned to the
versions the app was tested with: fastapi, uvicorn, sqlalchemy,
aiosqlite, alembic, faster-whisper 1.2.1, dev extras)
- editable install resolves 'shonar' to THIS backend/ (verified via
shonar.__file__); PYTHONPATH pin kept as a belt
- verified standalone: with ~/Projects/Shonar renamed away, engine boots,
self-migrates (alembic head m8models000001), /healthz + /readyz ok, and
full register -> upload -> finalize -> list -> audio-download roundtrip
passes (sha256 match)