Commit Graph

23 Commits

Author SHA1 Message Date
fegger 76cf992373 Desktop GUI: Library tab with live sync to the server
- new Record/Library tabs (like the mobile app); the recording UI is
  unchanged inside the Record tab
- Library tab: recording list (date, duration, language, device), detail
  view with summary (scrollable, re-triggerable), agenda checklist with
  coverage marks, and the full timestamped transcript; Library URL field
  (defaults to MEETREC_LIBRARY_URL)
- sync: auto-refresh every 10 s — recordings made on the phone appear on
  the desktop while the tab is open; pending summaries update to done
  automatically and re-fetch
- shared library client in meetrec.py: library_list/library_get_file/
  library_trigger_summary
- validated headlessly (offscreen) against the live server: 6 recordings
  from phone and desktop listed, 68-min meeting detail with 1333-char
  summary and ~100k-char transcript
2026-09-08 21:37:17 +02:00
fegger c88af68fd9 Android: make the meeting summary scrollable (max 240dp box) 2026-09-08 21:17:22 +02:00
fegger c4b86213d4 Desktop: M3 feature parity with the Android app
- final_transcribe gains optional speaker labeling and library upload:
  diarize_url runs a second tinydiarize pass and merges the turn times
  onto the quality transcript as 'Sprecher 1/2:' labels (diarize_merge,
  port of the Android Diarization object; failures keep the transcript
  unlabeled); library_url uploads the WAV + txt/srt/json bundle with the
  agenda — the server then generates summary + agenda coverage
- WhisperServerEngine supports diarize mode (tinydiarize form field +
  speaker_turn_next parsing); multipart builder handles multiple files
- CLI: --agenda-file, --diarize-url, --library-url (env vars
  MEETREC_DIARIZE_URL / MEETREC_LIBRARY_URL)
- GUI: Library URL + Diarize URL fields and an agenda editor; upload
  progress lands in the status bar
- validated end-to-end against the live servers: recording -> large-v3
  transcription -> txt/srt/json -> library upload (all four files,
  device=desktop) -> server summary/agenda pipeline ran (honest error
  for a noise-only test recording)
2026-09-08 21:04:53 +02:00
fegger 293ec8bdec Fix meeting summaries: long transcripts overflowed the LLM context
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)
2026-09-08 20:53:33 +02:00
fegger 76611af7e0 M3d: speaker labels via tinydiarize two-pass merge
- 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
2026-09-08 17:07:31 +02:00
fegger 6a400e842f Android UI: Settings tab, compact recording view, scrollable config
- 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)
2026-09-08 16:56:24 +02:00
fegger 2cf785746e M3c: meeting summaries and agenda coverage via Ollama
- 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)
2026-09-08 12:32:20 +02:00
fegger 4a2aeb60d8 Android M3b: Library tab — browse, play, share, delete recordings
- 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)
2026-09-08 12:26:33 +02:00
fegger 29bc4ef40b Android: fix upload lifecycle and JNI UTF-8 abort (M3a hardening)
- 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)
2026-09-08 11:42:00 +02:00
fegger 6b697f8d94 M3a: meetrec-server storage service + automatic upload from the app
- 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
2026-09-08 09:57:40 +02:00
fegger 50d00286d7 Android: remote transcription via whisper.cpp server (Phase 2)
- 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
2026-09-07 13:26:02 +02:00
fegger a1df9d4037 Add whisper-server engine: transcribe on a remote whisper.cpp server
- WhisperServerEngine posts multipart/form-data to POST /inference
  (verbose_json) with the same transcribe() contract as the other
  engines, so live and final passes work unchanged; stdlib-only
  multipart builder; HTTP and connection errors surface clean messages
- CLI: --engine whisper-server + --server-url (or MEETREC_SERVER_URL)
- GUI: engine dropdown gains whisper-server; model/device/compute grey
  out (the server owns the model), new Server URL field
- model lives server-side, beam size is a server-start setting in
  whisper.cpp v1.9.3 (documented); verbose_json reports language names
  (german) rather than ISO codes
- validated against the live R9700 server over Tailscale: array input,
  native-rate file input (server-side resample), full final_transcribe
  pipeline, and unreachable-server handling
2026-09-07 12:59:14 +02:00
fegger e97e8b1d33 whisper-server: validate model downloads (size + ggml magic), self-heal poisoned files
The first server deployment downloaded only 1101 bytes (a redirect/error
page) and the entrypoint promoted it to ggml-large-v3.bin, after which
every restart skipped the download and the server ran without a valid
model. Now both fresh downloads and existing files are validated
(minimum 50 MB + the ggml magic bytes); invalid files are logged,
deleted, and re-downloaded, and a failed download dumps the first 400
bytes of what was actually received for diagnosis before exiting.

Validated locally: a poisoned model file is detected, removed, and a
valid one re-downloaded; inference served correctly afterwards.
2026-09-07 12:37:38 +02:00
fegger ce1880dc90 whisper-server: move to port 8085 (8080 taken on the host)
Consistent across compose, entrypoint default, Dockerfile EXPOSE,
healthcheck, and README examples.
2026-09-07 12:27:44 +02:00
fegger ef59fc0fb4 Add dockerized whisper.cpp inference server (Vulkan GPU, Tailscale)
- server/whisper-server: compose stack built from the pinned whisper.cpp
  v1.9.3 release, same as the Android JNI layer
- Vulkan GPU backend (AMD Radeon AI PRO R9700 / RADV) with transparent
  CPU fallback and NO_GPU override; GGML models auto-download on first
  start (MODEL env, default large-v3)
- API bound to the Tailscale interface only (100.103.83.12:8080) since
  whisper-server has no authentication; render-group GID passthrough for
  /dev/dri
- validated locally: image builds, entrypoint downloads tiny, POST
  /inference returns verbose_json with language + segments; GPU-less
  fallback confirmed
2026-09-07 12:17:40 +02:00
fegger 46b0f128bc Android M2: live rolling transcript and automatic final pass
- 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.
2026-09-07 12:14:24 +02:00
fegger accfe15350 Android: replace language free-text with a dropdown (auto/en/de)
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.
2026-09-07 11:36:54 +02:00
fegger 8e079467fa Android M1: meeting recording via foreground service
- 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
2026-09-07 11:31:29 +02:00
fegger e5ec95a5fb Add app icon (mic + waveform) and fix desktop categories
- share/icons/hicolor/: SVG source + PNG fallbacks (48-256 px); the
  desktop entry now uses Icon=meetrec instead of a stock theme icon
- install.sh installs the icon into the hicolor tree, refreshes the
  GTK icon cache, and cleans up on uninstall
- Categories fixed to AudioVideo;Audio per the menu spec (bare Audio
  is a subcategory requiring its main category); desktop-file-validate
  is clean now
2026-09-07 11:09:00 +02:00
fegger e14a6bfe9f Record at the input device's native sample rate
- Recorder queries default_samplerate and streams/WAVs at that rate
  instead of forcing 16 kHz (helps devices that don't support it
  natively and keeps recordings at full fidelity)
- new resample_16k() linear resampler: the live rolling window is
  converted to Whisper's 16 kHz before being handed to the engines
- snapshot() now returns (16 kHz audio, absolute start time in s)
- final pass unchanged: faster-whisper (PyAV) and whisper.cpp
  (miniaudio) resample native-rate WAVs themselves
- GUI status shows the active rate; README updated

Validated with real 44.1 kHz and 48 kHz inputs: WAV headers match the
device rate, live-window resampling is length- and spectrum-correct.
2026-09-07 11:04:53 +02:00
fegger 404f3db200 Add native Android app scaffold (M0): whisper.cpp engine via JNI
- android/: Gradle/Kotlin project (AGP 9.4, Compose, NDK 27.1), monorepo
  subdir as planned; whisper.cpp v1.9.3 vendored via pinned fetch script
- core/whisper: JNI wrapper (beam size, threads, language) + WhisperEngine
  Kotlin API mirroring the desktop engine contract
- app (M0 scope): in-app GGML model download from Hugging Face, engine
  load, WAV picker with resampling, on-device transcription, share sheet
- build validated: assembleDebug OK, libwhisper_jni.so + ggml packaged
2026-09-07 11:00:31 +02:00
fegger 66408e291d Add whisper.cpp engine with Vulkan GPU support (AMD/Intel/NVIDIA)
- meetrec.py: engine layer with a common transcribe() contract;
  WhisperCppEngine shells out to whisper-cli, GGML + Silero VAD models
  auto-download to ~/.cache/meetrec; faster-whisper import now lazy;
  new --engine CLI option
- meetrec_gui.py: Engine dropdown; Device/Compute greyed out for
  whisper-cpp; model reload keyed on (engine, model, device, compute)
- install.sh: --whisper-cpp builds whisper.cpp with -DWHISPER_VULKAN=ON,
  installs whisper-cli into the prefix and wires WHISPER_CPP_BIN via a
  generated whisper-cpp.env; build failures degrade to a warning
- launchers source whisper-cpp.env; README documents engines
- Recorder.stop() is now idempotent (GUI close path called it twice)
2026-09-07 10:36:55 +02:00
fegger 18ce1c134c Initial commit: MeetRec — record & transcribe meetings with Whisper
- meetrec.py: CLI recorder with live rolling transcript (faster-whisper)
- meetrec_gui.py: PySide6 GUI reusing the CLI's recording/transcription
  logic, with model/device/compute selection and live transcript
- install.sh: local install into ~/.local (venv, deps, launchers,
  desktop entry), with --model/--no-model/--uninstall
- bin/meetrec, bin/meetrec-cli: launchers
- share/applications/meetrec.desktop: XDG desktop entry
- README.md, requirements.txt, .gitignore
2026-09-07 09:38:32 +02:00