Commit Graph

28 Commits

Author SHA1 Message Date
Brummel d65720c671 Refactor oneliner storage to use enum
The oneliner is now stored as `OnelinerState` enum which can represent
three states: `Ready`, `Empty` or `Error`. This allows the client to
differentiate between a case with no medical content and a case where
the oneliner generation failed.

The `OnelinerState` enum is serialized to JSON and stored in
`oneliner.json` file. The `OnelinerEntry` struct has been updated to
reflect this change.

The client-desktop application has been updated to handle the new
`OnelinerState` enum and display appropriate UI elements for each state.

The server-side code has also been updated to read and write the
`OnelinerState` enum, and to handle the new file format.
The `reset_case_artefacts` function in
`server/src/routes/case_actions.rs` has been updated to remove
`oneliner.txt` and create `oneliner.json` instead.

The tests have been updated to reflect these changes.
2026-04-20 12:37:48 +02:00
Brummel 224ee60363 feat: Handle empty Ollama responses gracefully
Introduce `OllamaError::EmptyResponse` to represent cases where the
Ollama
model returns an empty result due to silence rules or lack of keywords.

This change prevents treating an empty response as a parse error and
logs
it at a lower severity level. A new integration test verifies this
behavior.
2026-04-20 11:24:44 +02:00
Brummel 1d702e2d85 Add event bus for live UI updates
Introduces a system for broadcasting real-time events to the web UI.
This enables features like automatic page reloads when case data changes
in the background, improving the user experience by keeping the UI
synchronized with the backend.

Key components:
- `events` module: Contains the `CaseEventKind` enum, `CaseEvent`
  struct, and `EventSender` type for managing the broadcast channel.
- SSE endpoint (`/web/events`): Streams events to connected browsers.
- Client-side JavaScript: Listens for events and triggers debounced page
  reloads.
- Integration points: Workers and route handlers now emit events when
  relevant state changes occur.
2026-04-19 23:33:53 +02:00
Brummel 17aa5d7200 Implement audio range requests
This commit enables serving audio files via HTTP Range requests. This is
crucial for allowing HTML5 audio players to seek to specific positions
within an audio file without re-downloading the entire file.

The changes include:
- Modifying `handle_audio` in `server/src/routes/web.rs` to parse
  `Range` headers.
- Implementing `serve_range` to handle partial content responses.
- Adding a `parse_range` helper function.
- Updating tests to verify range request functionality.
- Adding `ACCEPT_RANGES: bytes` header to indicate support for range
  requests.
- Storing recording duration in a sidecar file for faster UI rendering.
- Enhancing the HTML template to support a custom audio player with
  seeking.
2026-04-19 22:26:04 +02:00
Brummel 0d5c2f5888 Formatting 2026-04-19 15:35:10 +02:00
Brummel a1ff410d1b Refactor ETag generation for oneliners API
Remove the per-user oneliner watermark, which was previously used to
generate ETags for the `/api/oneliners` endpoint. The watermark was
intended to track the last successful oneliner write for each user.

The ETag generation has been refactored to use a deterministic FNV-1a
hash. This hash is computed over a sorted list of tuples containing
`(case_id, last_recording_at_ns, oneliner_mtime_ns)` for all visible
cases. This approach ensures that the ETag changes whenever any
listen-relevant file system modification occurs, such as a new
recording, oneliner regeneration, soft-delete, undo, or
upload-auto-reopen. This new method is more robust and avoids the
complexity of managing per-user state.

The `OnelinerWatermark` type alias and its associated logic have been
removed from `AppState` and the relevant modules (`transcribe::worker`,
`transcribe::recovery`, `routes::oneliners`, `main`). New integration
tests have been added to verify that various delete operations (single
delete, undo delete, bulk delete) correctly trigger an ETag update,
ensuring cache invalidation.
2026-04-19 14:47:30 +02:00
Brummel 79e91246c6 Add last_failure to worker state
The `WorkerSnapshot` now includes `last_failure`, which tracks the time
of the last recorded error. This allows the UI to display a more
accurate status, especially when a failure occurs after a recent
success.

The `pick_footer_status` function has been updated to prioritize
`last_failure`. If a failure occurred more recently than the last
success, the footer will show `Offline`, regardless of the success age.
This addresses scenarios where the server might have temporarily failed,
and the UI should reflect that immediately.

The `run_loop` function now updates `last_failure` upon transient or
terminal upload errors and poll failures. This ensures that the new
state information is correctly propagated.
2026-04-18 13:45:21 +02:00
Brummel 94392e72d8 feat: Add API endpoint for listing oneliners
This commit introduces a new API endpoint `/api/oneliners` that allows
authenticated users to retrieve a list of their recent oneliners.

The endpoint supports conditional GET requests using the `ETag` header
for efficient caching. It also allows specifying a time window for the
oneliners via the `hours` query parameter, with sensible defaults and
clamping.

The implementation includes:
- A new route handler `handle_oneliners`.
- A new type `OnelinerWatermark` for tracking user-specific timestamps.
- Logic for scanning user data directories, filtering by time, and
  handling deleted cases.
- Test cases to cover various scenarios, including caching, time
  windowing, and edge cases.
2026-04-18 09:27:23 +02:00
Brummel 44bcc076b4 Defer oneliner regeneration until batch end
Avoid redundant LLM calls by waiting until all recordings in a batch
have been processed before regenerating the oneliner. This change
introduces a helper function `has_pending_recordings` to check for any
`.m4a` files without a corresponding transcript, and the
`update_oneliner` logic now only proceeds if no pending recordings are
found.
2026-04-16 21:10:55 +02:00
Brummel e64204b757 Feat: Add gazetteer normalization to transcription
This commit integrates the Gazetteer into the transcription pipeline to
normalize terminology.

The Gazetteer, loaded at startup, is now passed to the transcription
worker. Before persisting the transcription text, it is processed
through the Gazetteer's `replace` method, ensuring standardized
terminology. This normalization is also applied to the generated
"oneliner" text.

Additionally, the `regenerate_missing_oneliners` function in the
recovery module has been updated to accept and utilize the Gazetteer,
ensuring that regenerated oneliners also benefit from terminology
normalization.

A new integration test, `transcribe_worker_normalizes_whisper_output`,
has been added to verify that the Whisper output is correctly normalized
by the Gazetteer before being saved to the transcript file.
2026-04-16 20:43:04 +02:00
Brummel 44ed5e8333 Refactor ollama transcription prompts
The system prompt for the transcription Ollama LLM has been updated to
improve robustness and clarity, especially for handling empty or
non-medical transcripts.

The prompt has been refactored to:
- Use English for instructions to improve robustness with smaller LLMs.
- Clearly define the behavior for empty or non-medical transcripts,
  requiring an empty string response.
- Reduce the maximum character limit from 120 to 60 to better fit
  smartwatch displays.
- Ensure the output is German, adhering to the original language
  requirement for the output.
- Add an explicit instruction to correct transcription errors in the
  input dictation for the consolidation LLM.
2026-04-16 15:30:45 +02:00
Brummel b1a8186fcd Add recovery for missing oneliners
This commit introduces a recovery mechanism to regenerate `oneliner.txt`
files for cases where transcription was successful but the oneliner
generation failed due to a crash between writing the transcript and the
oneliner.

The `transcribe::recovery::regenerate_missing_oneliners` function is
added and called at startup after `scan_and_enqueue`. This function
identifies cases with non-empty transcripts but missing oneliners and
triggers their regeneration.

The `transcribe::worker::update_oneliner` function is refactored to
always regenerate the oneliner from all available transcripts in a case,
ensuring that later recordings can correct earlier ones. This replaces
the previous `ensure_oneliner` logic which only generated the oneliner
if it didn't exist.

New helper functions `cases_needing_oneliner` and
`all_transcripts_joined` are introduced to support these changes.
Comprehensive unit tests are included for the new recovery and file
processing logic.
2026-04-16 14:43:54 +02:00
Brummel bbc9b53609 Refine system prompt and normalize logic
The system prompt has been updated to include an explicit instruction
for the LLM to return an empty string if no suitable keyword is found.
This improves robustness by handling cases where the input transcript
might not contain easily extractable medical terms.

Additionally, the `normalize` function's logic for finding the first
non-empty line has been slightly refined to use a more idiomatic Rust
approach by chaining `.lines().find(...)` and `.unwrap_or("")`. This
change improves code clarity and maintainability without altering the
functional behavior of finding the first non-empty line.
2026-04-16 02:08:24 +02:00
Brummel 990d166617 Refactor case listing and silent recording handling
The case listing on the "My Cases" page is now grouped by local date,
with labels for "Heute", "Gestern", or the ISO date. This improves
readability and organization.

Additionally, the handling of silent recordings has been refined.
Previously, the absence of usable recordings would result in an error.
Now, the analysis worker gracefully handles this by writing a stub
document and skipping the LLM call. This prevents unnecessary errors and
provides a clearer status for silent cases.

The `dictate.sh` script has been updated to simplify the case directory
lookup logic. Instead of iterating through `open` and `done`
subdirectories, it now directly checks for the case ID and ensures it's
not marked as deleted. This simplifies the script and improves
efficiency.

The `server/Cargo.toml` and `server/Cargo.lock` have been updated to
include the `num_threads` dependency and enable additional features for
the `time` crate, which are necessary for proper local time zone
handling.
2026-04-16 01:10:52 +02:00
Brummel 2f62f11563 Refactor busy flags and worker guards
The `WorkerBusy` type was previously a single `Arc<AtomicBool>` shared
between the analyze and transcribe workers. This commit refactors this
to:

- Introduce `AnalyzeBusy` and `TranscribeBusy` newtype wrappers around
  `WorkerBusy` to distinguish between the two flags. This allows
  `axum::extract::State` to target them individually.
- Move the `BusyGuard` RAII guard into `src/lib.rs` and make it generic
  to work with any `WorkerBusy` instance.
- Update the `main.rs`, `worker.rs`, and `routes/user_web.rs` files to
  use the new types and guards, ensuring correct state management for
  both pipelines.
- Enhance the transcription recovery scan
  (`transcribe::recovery::scan_and_enqueue`) to iterate over user
  directories and call `enqueue_pending_for_user` for each, making it
  more robust and aligned with the per-user self-healing mechanism.
- Add a check for `transcribe_busy` in the `case_detail.html` template
  to correctly display "Transkription läuft…" only when the transcribe
  worker is actually active.
2026-04-16 00:32:45 +02:00
Brummel 0b63d8ff56 Feat: Add bulk actions for case analysis and deletion
Introduces a new `/web/cases/bulk` endpoint to handle multiple case
actions simultaneously.

This change adds the following:
- A new `bulk` module for handling bulk operations.
- The `handle_bulk_action` function in `bulk.rs` to dispatch to specific
  actions.
- `bulk_analyze` and `bulk_delete` functions to process the actions.
- Updates `Cargo.toml` and `Cargo.lock` to include necessary
  dependencies: `form_urlencoded`, `serde_core`, `serde_html_form`, and
  `serde_path_to_error`.
- Modified `axum-extra` features to include `form`.
- Adds checks for deleted cases in `collect_pending` for both analysis
  and transcription recovery.
- Renames and modifies `handle_close_case` to `handle_analyze_case` in
  `case_actions.rs` to better reflect its functionality.
- Adds a new `handle_delete_case` and `handle_undo_delete` to
  `case_actions.rs`.
- Updates `my_cases.html` and `case_detail.html` to support bulk
  actions, including checkboxes, a bulk action bar, and an "Undo last
  delete" feature.
- Modifies `handle_audio` and `scan_cases` in `web.rs` to respect delete
  markers.
- Updates `analyze_test.rs` with new request builders for analyze and
  delete actions.
2026-04-15 23:06:31 +02:00
Brummel 11eb645f3f Refactor: Simplify case directory structure
The distinction between `open/` and `done/` directories for cases has
been removed.
All cases for a user now reside directly under the user's directory
(e.g., `<data_path>/<slug>/<case_id>/`).

This simplifies path management and eliminates redundant directory
traversals.

Key changes include:
- Removed `open/` and `done/` subdirectories in path resolution.
- Introduced a `paths::case_dir` function as a single source of truth
  for case directory layout.
- Updated various modules (`recovery`, `auth`, `routes`, `transcribe`,
  `tests`) to use the new path structure.
- Adjusted templates to reflect the simplified case status
  representation.
2026-04-15 22:15:13 +02:00
Brummel 61b5c8e125 feat: Mark failed recordings with .m4a.failed
Add a new `failed` field to `RecordingView` and corresponding logic in
the worker.
When FFmpeg remuxing or Whisper transcription fails, the worker now
renames the
original `.m4a` file to `.m4a.failed`. This prevents the recovery scan
from
reprocessing these files and signals to the UI that transcription
failed.

The UI displays a message indicating failure and suggests renaming the
file
back to `.m4a` to retry. A new integration test
`worker_renames_audio_to_failed_on_whisper_error`
is included to verify this behavior.
2026-04-14 14:32:22 +02:00
Brummel b63a269776 feat: Forward user settings to Whisper API
Include per-user hotwords and initial prompts in the multipart form data
sent to the Whisper API. The language parameter is now dynamically set
based on user configuration, defaulting to "de" if not specified.
2026-04-14 11:04:28 +02:00
Brummel fc8efc62e0 Add user slug to transcription jobs
The `TranscribeJob` struct now includes a `user_slug` field. This is
necessary for the transcription worker to look up per-user settings such
as hotwords and initial prompts. The `collect_pending` function has been
updated to return user slugs along with audio paths, and the recovery
scan now enqueues jobs with the correct user slug.
2026-04-14 11:02:37 +02:00
Brummel 6dac672089 Tighten oneliner prompt: keywords, no adjectives, no therapy
Previous prompt allowed 80 chars and evaluative adjectives, producing
oneliners like 'HOCM, massive Septumhypertrophie, LVT, Indikation
CASH/Myektomie'. Reframed from 'Hauptdiagnose' to keyword extraction
since gemma cannot reliably identify primary diagnoses.
2026-04-14 00:22:36 +02:00
Brummel 33da841087 Add oneliner generation from transcriptions
This commit introduces functionality to automatically generate a
one-liner summary from a completed transcription.

The `ensure_oneliner` function checks if `oneliner.txt` already exists
in the case directory. If not, it uses the Ollama API to generate a
summary from the full transcript. Any errors during generation or
writing are logged but do not halt the transcription process.
2026-04-13 17:29:58 +02:00
Brummel 47829b953c Add ollama module for LLM summarization
Introduces a new module `ollama` to handle communication with an Ollama
LLM for generating concise, one-line summaries of medical transcripts.
This module includes:

- `generate_oneliner`: The main function to interact with the Ollama
  API.
- `normalize`: A pure function for cleaning and formatting the LLM's
  output according to specific rules.
- Error handling for API communication and response parsing.
- Unit tests for the normalization logic.
2026-04-13 17:27:23 +02:00
Brummel 1e3cc9574c Refactor user config validation and add transcript display
Introduces a dedicated function `validate_and_index_users` to
encapsulate the logic for validating user configurations, checking for
duplicate slugs and API keys. This function returns a `Result` to handle
errors gracefully, and its usage in `load_users` is updated to panic
with a more informative error message.

Additionally, this commit modifies the web routing to fetch and display
audio transcripts. When scanning recordings, it now attempts to read a
corresponding `.transcript.txt` file. If found, the transcript content
is included in the `RecordingView` and rendered in the `cases.html`
template. If the transcript file is not found, a "Transcription pending"
message is displayed.

The `transcribe` function in `whisper.rs` is updated to explicitly set
the language to German (`language=de`), which can improve transcription
accuracy by skipping language detection. The `encode=false` query
parameter has been removed, as the service is designed to handle encoded
audio.

Finally, tests have been added for the new `validate_and_index_users`
function to ensure duplicate slugs and API keys are correctly rejected.
Integration tests in `web_test.rs` have been updated to verify the
rendering of transcripts and the pending status.
2026-04-13 17:08:21 +02:00
Brummel 13375b8297 feat: Add recovery scan for pending recordings
This commit introduces a new module `transcribe::recovery` and a
function `scan_and_enqueue`.
This function is called at startup to find audio files that have not yet
been transcribed
and re-enqueues them for processing. This ensures that recordings are
not lost if the
server restarts before transcription is complete.

A new integration test `recovery_enqueues_only_pending_recordings` has
been added to
verify the functionality.
2026-04-13 16:47:38 +02:00
Brummel bcae4e5466 feat: Add transcription worker and state management
Introduce an `AppState` struct to hold shared application state,
including configuration and the transcription sender.
Modify `create_router` and `create_router_with_state` to use `AppState`.
Update `main.rs` to initialize the transcription worker and pass the
`AppState` to the router.
Implement the transcription worker logic in
`server/src/transcribe/worker.rs`, handling job queues, FFmpeg remuxing,
and Whisper transcription.
Update `server/src/auth.rs` to use `Arc<Config>` from the shared state.
Modify `server/src/routes/upload.rs` to enqueue transcription jobs and
handle potential errors.
Add `TranscribeJob` and channel types in `server/src/transcribe/mod.rs`.
2026-04-13 16:45:24 +02:00
Brummel 84f8df2bf4 feat: Add Whisper transcription client
Introduces a new module for interacting with the Whisper ASR service.
This includes
an async function to POST audio files and retrieve transcripts. Error
handling
for network and HTTP status codes is also implemented.

New tests are added to cover successful transcription, HTTP error
responses,
and request timeouts.
2026-04-13 16:40:20 +02:00
Brummel 73692b0a02 feat: Add ffmpeg remuxing for faststart 2026-04-13 16:37:52 +02:00