- new icon source in icon/ (make_icon_v3.py + masters): near-black
gradient tile, red record button with white bezel, mic capsule as a
transcript card with red lines and a cursor
- Android: adaptive icon uses the 512px master as the full-bleed
background layer (launcher masks crop it to shape) with a transparent
foreground; old vector foreground and launcher color removed
- desktop: hicolor PNGs replaced at 32-512 px (48 regenerated from the
master), old SVG removed; install.sh picks them up unchanged
- APK installed on the Fairphone; aapt2 confirms the adaptive icon
- summary: SelectionContainer (select text directly), Kopieren
(clipboard + toast), Teilen (share sheet) and Datei speichern (written
to Download/meetrec via MediaStore, no permission needed on API 29+)
- transcript: Datei speichern alongside the existing share
- installed and verified building; sample of summary export names:
meetrec-summary-<recording-id>.md / meetrec-transcript-<id>.txt
Reproduced on the 68-min meeting: the timestamped transcript filled
the whole 32k window (prompt_eval 32451), gemma4 hit the length limit
and returned an EMPTY answer (done_reason=length) — which the server
wrote as an empty summary.md and marked "done". Fixes:
- num_ctx 32768 -> 65536 and transcript cap 90k -> 100k chars (env
OLLAMA_NUM_CTX / TRANSCRIPT_MAX_CHARS); ~33k tokens now fit with
plenty of room for the answer
- _ollama_chat treats an empty response as an ERROR (with done_reason
and prompt_eval in the message) instead of producing a done-with-empty
summary
- truncated transcripts get a note appended to the summary
- startup recovery: a container restart resets stale "pending" statuses
so the UI can never stick on "wird erstellt" from dead threads
- phone: the summary/agenda poll survives transient fetch errors
(previously one network hiccup stopped the poll forever)
- whisper-server stack: second container (port 8086) running the
English-trained small.en-tdrz model with -tdrz; image patched
(speaker-turn.patch) to expose speaker_turn_next per segment in
verbose_json like the cli example does
- core/whisper Diarization: merges the tdrz pass's TURN TIMES onto the
quality transcript as alternating 'Sprecher 1/2:' labels, splitting
segments when a turn falls inside them; no turns detected = no
labels (never mislabels); 6 unit tests
- RemoteWhisperEngine gains a diarize flag (sends tinydiarize=true,
parses speaker_turn_next); WhisperEngine.Segment carries the flag
- RecorderService: optional second pass on the diarize server after the
final pass; failures keep the unlabeled transcript
- Settings: Diarize server URL (persisted; empty disables)
- validated infrastructure locally: patched image builds, tdrz model
downloads from akashmjn/tinydiarize-whisper.cpp, speaker_turn_next
present in responses; synthetic espeak audio does not trigger the
model's turn tokens — real two-person speech needed for the
end-to-end check
- new Settings tab holds Transcribe on, Server URL, Library URL, Model,
Language, Live model and the Download/Load engine actions (all
persisted); Record tab keeps agenda, record, tools and transcripts
- in-recording view hides all inputs: only level meter, agenda items,
status, live transcript and the Stop button
- idle config is scrollable with status + Record button fixed at the
bottom
- fix: the loaded engine and transcript lived in per-screen remember
state and were silently lost on tab switches; now shared via AppState
(engine is a heavyweight native object)
- meetrec-server: Ollama integration (chat API, gemma4:12b, num_ctx
32768); German structured summary (topic/points/decisions/to-dos)
written to summary.md; agenda coverage returns strict JSON (covered,
time, evidence) parsed defensively; both run automatically in a
background thread after upload plus manual trigger endpoints
(POST /summary, POST /agenda) with status tracking in the index
- phone: agenda input on the Record tab (one item per line, persisted)
is uploaded with the recording; Library detail shows the summary and
a per-item agenda checklist with timestamps and evidence quotes,
with polling while the server generates and manual re-trigger buttons
- validated end-to-end against the live Ollama server: crafted German
test meeting produced a correct structured summary and perfect agenda
discrimination (covered items with correct timestamps + quotes,
undiscussed item correctly false)
- new bottom navigation: Record | Library; existing screen moved to
RecordScreen.kt, new MainActivity holds the tab scaffold
- LibraryScreen lists recordings 'on this phone' (local files, showing
which have transcripts) and 'on the server' (meetrec-server index with
date, duration, language, device), with a refresh button
- detail view: timestamped transcript from meeting.json (txt fallback),
streaming audio playback via MediaPlayer (server WAV or local file),
share as timestamped text, delete with confirmation (server API or
local files)
- StorageClient gains list/fetchFile/delete; verified against the live
server on-device (list, detail, playback of the recovered 68-min
meeting)
- upload ran as a detached coroutine while cleanup() stopped the
service, and onDestroy's scope.cancel() killed it before any status
appeared; the upload is now part of finishRecording itself and the
service stays foreground ("Saving recording…") until it completes
- JNI getTextSegment used NewStringUTF, which ABORTS THE PROCESS on
invalid UTF-8 — whisper can emit garbled bytes on noisy audio (seen
live: SIGABRT with illegal start byte 0x8d); whisper_jni.cpp now
decodes UTF-8 to UTF-16 itself, replacing bad sequences with U+FFFD
- validated on device end-to-end: record -> local final pass -> automatic
upload -> library entry with all four files (verified server-side)
- server/meetrec-server: FastAPI storage API (upload bundle with wav/
txt/srt/json + metadata incl. agenda, list, fetch, download, delete);
file-based index.json, no database; Docker Compose on port 8090,
Tailscale-only bind like whisper-server; Ollama env prepared for M3c
(gemma4:12b, German)
- phone: StorageClient (stdlib multipart upload); RecorderService uploads
the bundle in the background after the final pass and publishes
UploadState (Uploading/Done/Error) to the UI
- app: Library URL setting (persisted, default http://100.103.83.12:8090,
empty disables upload); status line reports upload progress
- storage API validated locally end-to-end: upload, list, metadata,
download, path-traversal rejected, delete
- new Transcriber interface makes the recorder service engine-agnostic;
WhisperEngine (local JNI) and new RemoteWhisperEngine (POST /inference,
verbose_json, in-memory WAV upload via new WavEncoder) implement it
- SessionConfig carries serverUrl; when set, BOTH the live pass and the
final pass transcribe remotely (server owns the model, e.g. large-v3
on GPU); local engine remains the offline fallback
- UI: Transcribe on: phone/server dropdown + server URL field,
persisted in SharedPreferences; model controls grey out in server
mode; manual Transcribe also routes to the server
- network security config permits cleartext HTTP for user-configured
LAN/tailnet servers (documented; HTTPS works either way)
- WavEncoder round-trip unit test (11 total green); validated phone ->
Tailscale -> R9700 with large-v3 before the app-side change
- RecorderService: rolling-window live pass with a dedicated live model
(off/tiny/base, beam 1, 8 s ticks, time-based dedupe) and an automatic
final pass with the selected model (beam 5) writing txt/srt/json
- TranscriptFiles in core/whisper: desktop-compatible outputs, 5 JVM tests
- WhisperEngine exposes modelName; UI: live-model dropdown, merged
transcript view, share sheet; falls back to manual path without engine
- live loop failures now logged (MeetRec tag) and surfaced in the UI
(was silently swallowed), plus final-pass timing logs
On-device measurements (Fairphone 6): tiny live ~0.6x realtime, small
final ~0.8x realtime — motivates the planned whisper-server engine.
Fixed list per current needs; "auto" passes null to the engine like
the desktop app. Live-mode model defaults (tiny/base live, small for
final) will follow with M2.
- core/recording: MeetingRecorder (AudioRecord at the device's native
rate, WAV on disk + 60 s rolling window resampled to 16 kHz), streaming
WavWriter, thread-safe RollingWindow, linear resampler; WavReader moved
here from the app
- app: RecorderService (foreground, type microphone) with ongoing
notification and StateFlow state; Record/Stop UI with timer and level
meter; runtime permission flow; finished recordings auto-load for
transcription
- launcher icon (mic + waveform, matching the desktop brand) and
notification glyph
- fix real M0 bug caught by the new unit tests: WavReader parsed 16-bit
fmt fields (audioFormat/channels/bitsPerSample) with 32-bit reads
- JVM tests: WAV round trip, native-rate header, resampler, rolling
window — 5/5 green; assembleDebug and aapt2 APK checks pass
- validated on-device on a Fairphone 6 (Android 16): model download,
engine load, recording and transcription all working