Commit Graph

120 Commits

Author SHA1 Message Date
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 2bcdb5436e Refactor: Extract client core logic into separate crate
Introduce `doctate-client-core` to house UI-agnostic client business
logic. This promotes code reuse and allows consumption by different
client platforms, such as iOS via UniFFI bindings.

This commit also updates `client-desktop` to depend on
`doctate-client-core` and removes duplicated logic.
2026-04-19 18:46:17 +02:00
Brummel 9cc4dc6384 feat: Add admin view toggle to web UI
Introduces a toggle in the web UI that allows administrators to switch
between a standard view and an "admin view". This admin view exposes
additional administrative elements that are hidden in the standard view.

The toggle state is persisted in local storage, allowing the user's
preference to be remembered across sessions. Non-administrator users
will not see the toggle.
feat: Add admin view toggle to web UI

Introduce a client-side toggle for an "admin view" in the web UI. This
allows administrators
to selectively hide or show elements intended only for administrative
purposes. The toggle
state is persisted in local storage for a persistent user experience.

The implementation involves:
- Adding a checkbox element to the header of the case pages and my cases
  list.
- Using CSS to conditionally hide elements with the `admin-only` class
  when the admin view is off.
- Implementing JavaScript to manage the toggle's state, update local
  storage, and apply the
  `admin-view-off` class to the `<html>` element.
- Updating relevant templates (`case_page.html`, `case_recordings.html`,
  `my_cases.html`)
  to include the toggle and the `admin-only` class where appropriate.
- Adding unit tests to verify the visibility of the toggle for admins
  and non-admins.
2026-04-19 18:46:11 +02:00
Brummel 6369ff1680 feat: Add admin flag to user and templates
Passes an `is_admin` flag to the `CaseRecordingsTemplate` and
`case_page.html`.
This flag determines whether the full case ID is displayed in the meta
section
for administrative users. It also influences the header display on the
case page
to prioritize the oneliner when available.
2026-04-19 17:33:29 +02:00
Brummel b7e54db54c Refactor case detail into a dedicated page
This commit restructures the web interface for case details. The
previous `/web/cases/{case_id}` route, which previously showed both the
case summary and its recordings, has been split into two distinct pages:

- `/web/cases/{case_id}`: This page now displays the overall case
  information, including the document if available.
- `/web/cases/{case_id}/recordings`: This new page is dedicated to
  listing and displaying individual recordings within a case.

This change improves the organization and clarity of the web UI,
allowing for more focused views of case data. Additionally, the
`case_detail.html` and `document.html` templates have been removed as
their functionality is now handled by the new `case_page.html` and the
upcoming document rendering logic. The `cases.html` template has also
been removed, indicating a shift towards more granular page views.
2026-04-19 17:11:29 +02:00
Brummel 3bb2d23adb Refactor case pages into two distinct routes
This commit separates the case detail page into two distinct routes:
`/web/cases/{case_id}` for the main case information and document, and
`/web/cases/{case_id}/recordings` for a dedicated view of audio files
and their transcripts.

This change improves the organization and clarity of the case viewing
experience by segmenting the related but distinct information into their
own UI sections.
2026-04-19 17:02:16 +02:00
Brummel 76e8ee18e9 feat: Implement magic link authentication
This commit introduces a new magic link authentication flow.
The desktop client can now request a temporary, one-time-use token from
the server.
This token is then used to open a URL in the system browser, which
redirects to the server.
The server consumes the token, installs a regular web session, and
redirects the user
2026-04-19 16:00:12 +02:00
Brummel 0d5c2f5888 Formatting 2026-04-19 15:35:10 +02:00
Brummel 041f9015ca refactor(server): split build_analysis_input into pure + I/O parts
Replace the imperative loop that mixed mtime-tracking, transcript
reading, and validation with three clearly-scoped functions:

- compute_last_mtime: pure reduction over all m4as, returns max mtime.
- read_recordings: I/O loop that reads each transcript sidecar,
  aborts on missing transcript (legacy "Nicht alle Aufnahmen sind
  transkribiert" 400 body), silently skips blank ones.
- build_analysis_input: orchestrator that composes the above with
  RFC3339 formatting.

Error bodies are preserved byte-for-byte. Replaces a one-pass loop
with two passes; n <= ~10 in practice - negligible.
2026-04-19 15:22:45 +02:00
Brummel bce93811a0 refactor(server): split scan_user_cases into filter + view helpers
The 73-line function mixed three concerns: directory traversal,
UUID/deletion filtering, and view assembly. Split into:

- scan_user_cases: orchestrator (loop + sort).
- filter_valid_case_dir: gate that returns (case_id, case_path) for
  every valid case directory, skipping non-UUID names, files, and
  soft-deleted cases.
- compute_case_view: view assembly that returns None for empty
  cases (no recordings yet).

Each helper is now individually inspectable; nesting depth drops
from 3-4 to 2 levels. Let-else guards replace nested matches.
2026-04-19 15:19:20 +02:00
Brummel e995cdd7c4 refactor(server): bundle worker pipeline into PipelineState
Group the four worker handles (analyze/transcribe x busy/tx) into a
new PipelineState struct with a FromRef<AppState> impl. Convert
heal_orphans_if_idle from a free function with 6 parameters into a
method on PipelineState. The two web handlers (handle_my_cases,
handle_case_detail) now take one State<PipelineState> instead of
four separate State extractors.

The original FromRef impls for AnalyzeBusy/Sender and TranscribeBusy/
Sender remain intact so handlers that need only one of them keep
their narrower signature.
2026-04-19 15:14:22 +02:00
Brummel 21534ab4d1 refactor(server): extract locate_case_or_404 helper
Consolidate 5 instances of "locate_case + warn! + 404-return"
boilerplate (case_actions.rs x4, user_web.rs x1) into a single
pub(crate) helper in user_web.rs. The helper takes &CaseId (no
String allocation at call sites) plus a context tag that becomes
a structured-log field instead of being baked into the message.

bulk.rs keeps using the underlying locate_case because its miss
semantics differ (silent skip, not 404).
2026-04-19 15:11:09 +02:00
Brummel 16c6fb2e07 refactor(server): introduce CaseId newtype with typed Path extractor
Replace five duplicated uuid::Uuid::parse_str calls in web handlers
(analyze, document view, delete, reset, case detail) with a
CaseIdPath extractor that validates the URL segment once and
returns the legacy "Invalid case_id" 400 body byte-for-byte.

The extractor uses axum's FromRequestParts and maps any failure
to AppError::BadRequest, so IntoResponse handling is unchanged.
2026-04-19 15:05:46 +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 5e8d2f8e58 Refactor case store to support deletions
Introduce `reconcile_with_server_snapshot` to the `CaseStore`. This
function
removes local markers that are synced to the server and within the
server's reported window but are no longer present in the server's
snapshot. This ensures that cases deleted server-side are also removed
locally, fixing an issue where deleted cases would still appear on the
client.

This change also modifies the `poll_once` function in `server_sync`
to call both `merge_server_snapshot` and the new
`reconcile_with_server_snapshot` after a successful poll.

Additionally, a test case `poll_once_reconciles_missing_synced_marker`
has been added to verify the correct behavior of the reconciliation
process.

The server-side upload handler is updated to automatically reopen
soft-deleted cases if a new upload is received for them. This ensures
that soft-deleted cases reappear in the API and UI without manual
intervention. Two new tests, `upload_reopens_soft_deleted_case` and
`upload_resurrects_hard_deleted_case`, are added to cover the
soft-delete
reopening and hard-delete resurrection scenarios, respectively.
2026-04-19 13:43:26 +02:00
Brummel 5d308df2b5 Add TOML dependency and new config/startup modules
The TOML dependency is added to `Cargo.lock` to support configuration
file parsing.
New modules `config` and `startup` are introduced in
`doctate-client-core`:
- `config`: Handles client configuration loading and saving, including
  defaults for polling intervals and window hours.
- `startup`: Contains routines for initial cleanup tasks, such as
  removing stale markers and orphan audio files based on defined
  retention periods.
2026-04-18 14:09:26 +02:00
Brummel 5b17c331c9 Add pending upload indicator to UI
This commit introduces a visual indicator in the UI to show when a case
has pending uploads.

The `WorkerSnapshot` struct has been updated to include a
`pending_case_ids` field, which is an `Arc<HashSet<Uuid>>`. This set
stores the IDs of cases that currently have files in the pending
directory.

The `run_loop` function now populates this set by scanning the pending
directory and collecting the `case_id`s of any files found. This
snapshot is then sent to the UI via the `worker_rx` channel.

In the `DoctateApp::ui` method, the `pending_ids` set is cloned from the
worker's snapshot. If a case's ID is present in this set, a distinct "⬆"
prefix and a yellow label are used to highlight it, signifying an
ongoing or pending upload. Otherwise, the standard label is used.
2026-04-18 13:51:11 +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 305d739f56 Refactor upload and polling logic
This commit consolidates the upload and polling functionalities into a
single `ServerSync` worker. The `oneliner_poller` and `uploader` modules
have been removed, and their responsibilities are now handled by
`server_sync`.

The `ServerSync` worker manages both uploading pending recordings and
polling the `/api/oneliners` endpoint. Uploads take precedence, and
polling only occurs when the pending upload directory is empty. The
worker now uses a unified HTTP client and a single `tokio::select!` loop
for managing these tasks.

Key changes include:
- Removal of `oneliner_poller.rs` and `uploader.rs`.
- Introduction of `server_sync.rs` and `upload.rs` in
  `doctate-client-core`.
- `ServerSync` now handles upload events (`UploadEvent`) and provides a
  `WorkerSnapshot` for UI feedback.
- Upload logic has been refactored to handle transient and terminal
  errors more robustly, including file deletion for terminal failures.
- Polling logic is integrated into the `ServerSync` loop, utilizing the
  existing `SnapshotCache` and `CaseStore`.
- The `App` component in `client-desktop` has been updated to use
  `ServerSync` instead of the separate poller and uploader.
- Documentation comments have been updated to reflect the new structure.
2026-04-18 13:24:38 +02:00
Brummel cab71e6a26 feat: Add startup cleanup for stale markers and orphans
This commit introduces a new startup cleanup routine for the Doctate
client.
The cleanup process reaps stale markers and orphaned audio files to
adhere to data minimization principles and maintain client efficiency.

Specifically, it performs the following actions:

- Removes markers that are older than 72 hours and have been confirmed
  by
  the server. This ensures that old, irrelevant metadata is purged.
- Cleans up orphaned audio files (e.g., `.m4a` without corresponding
  `.meta.json` or vice-versa) that are older than 24 hours. This
  prevents the accumulation of audio data that will never be uploaded,
  as such files are silently ignored by the uploader.
- Removes any temporary files (`.tmp`) left over from interrupted
  writes,
  as these are considered invalid at startup.

The `filetime` crate is added as a dependency to facilitate setting
modification times for testing purposes.
2026-04-18 12:22:36 +02:00
Brummel bb29671733 Implement single-instance lock
Add a mechanism to prevent multiple instances of the desktop client from
running simultaneously. This is achieved by creating a lock file. The
first instance successfully acquires the lock, while subsequent attempts
by other instances will fail, prompting them to exit gracefully. This
ensures data integrity and prevents potential conflicts.
2026-04-18 11:37:16 +02:00
Brummel cdfa5ae90e Refactor timestamp handling for activity sorting
Introduce `last_recording_at` to `OnelinerEntry` and use it as the
primary sort key for cases. This ensures that cases with recent
dictation activity are prioritized, even if their initial creation date
is older.

The logic for determining a case's activity has been updated to consider
the most recent `.m4a` file's modification time (`last_recording_at`),
falling back to `updated_at` or `created_at` if necessary.

This change also refactors the timestamp formatting and handling within
the web interface to correctly display and sort cases based on their
actual last activity time, improving user experience and data relevance.
The `now_rfc3339` function is updated to strip sub-second precision for
consistent filename generation.
2026-04-18 11:33:35 +02:00
Brummel 7637596598 feat: Add poller and case list to desktop client
This commit introduces a new poller to the desktop client, responsible
for fetching case markers and their one-liners. It also integrates a
case list UI, allowing users to view and interact with their cases.

Key changes include:

- **New Dependencies**: Added `doctate-client-core`, `time`, and
  `webbrowser` to `Cargo.lock`.
- **Background Task Spawning**: Refactored `spawn_uploader` to
  `spawn_background` to include the new `OnelinerPoller` and its
  dependencies.
- **Case Store Integration**: The `DoctateApp` now initializes and uses
  a `CaseStore` to manage case markers.
- **UI Enhancements**:
    - A new `render_case_list` function displays recent cases with their
      last activity time and one-liner.
    - Buttons to "Continue" recording for a case and "Open" the case in
      a web browser are added.
    - A staleness indicator for the poller's connection is displayed.
- **State Management**:
    - The `AppState` is updated to reflect the new UI elements and
      interactions.
    - Actions like `Continue` and `OpenWeb` are handled.
- **Configuration**: Poll interval and window hours are now configurable
  and used by the poller.
- **Error Handling**: Improved error handling for saving configuration
  and background task operations.
- **Code Cleanup**: Minor refactoring and documentation updates.
2026-04-18 10:12:38 +02:00
Brummel 19d841e644 Add new config fields for oneliner polling
Introduces `oneliner_poll_interval_seconds` and `oneliner_window_hours`
to the client configuration. These fields allow users to customize how
frequently the client polls for new oneliners and the time window for
the data fetched from the server.

Default values are provided for these fields to ensure backward
compatibility and a sensible out-of-the-box experience. The `Config`
struct now also includes helper methods `poll_interval()` and
`window_hours()` for easier access to these settings, including the
application of defaults.

Additionally, new functions `cases_dir()` and `snapshot_cache_path()`
are added to `client-desktop/src/paths.rs` to manage directories for
case data and the oneliner snapshot cache, respectively. These paths
follow OS-standard conventions for local application data.
2026-04-18 10:09:00 +02:00
Brummel 9b9d68402f Refactor: Implement oneliner poller background task 2026-04-18 10:07:28 +02:00
Brummel 1b4864215b feat: Implement snapshot cache persistence
Add `SnapshotCache` struct to handle on-disk caching of API responses.
This includes `load`, `store`, and `clear` methods for managing the
cache file.
Atomic writes are implemented using a temporary file and rename
operation.
Error handling for file operations and JSON deserialization is included.
Unit tests are provided to verify cache functionality.
2026-04-18 10:05:10 +02:00
Brummel ac7157cdca feat: Add doctate-client-core crate
Introduces a new crate `doctate-client-core` to house shared client-side
logic. This includes:

- `case_store`: Manages local case marker files and merging with server
  snapshots.
- `snapshot_cache`: Placeholder for caching server responses.
- `oneliner_poller`: Placeholder for the background polling task.

The workspace configuration and `Cargo.lock` have been updated to
include the new crate.
2026-04-18 10:03:40 +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 6fbb549ddf Update project plan for desktop client
Refine documentation regarding the desktop client, including its
architecture, tech stack, and development rationale. Emphasize the
choice of `eframe`/`egui` over alternatives like Tauri and Compose
Multiplatform, and explain the decision to use `ffmpeg` as a subprocess
for audio recording and graceful stopping. Clarify the rationale for
prioritizing the desktop client's development.
2026-04-17 16:47:22 +02:00
Brummel d0f70e706e Add libc dependency for unix targets
This commit adds the `libc` dependency to the `Cargo.toml` file for
Unix-based targets. This dependency is required by the `ffmpeg` command
execution within the `recorder` module, specifically for handling
process management and signaling on Unix-like systems.
2026-04-17 16:23:04 +02:00
Brummel 5f7e46256c Add audio recording and upload capabilities
Introduces new modules for audio recording (`recorder.rs`) and
asynchronous uploading (`uploader.rs`) with retry logic.

The `recorder` module uses `ffmpeg` as a subprocess to capture audio and
save it as M4A files. It handles starting, stopping, and reporting
recording events.

The `uploader` module manages a queue of recordings to be uploaded to
the server. It supports persistent storage of pending uploads via
sidecar JSON files, exponential backoff for retries, and emits events
for UI feedback.

New dependencies were added to `Cargo.toml` and `Cargo.lock` to support
these features, including `reqwest`, `serde_json`, `tokio`, `uuid`, and
`which`.
2026-04-17 15:49:55 +02:00
Brummel c441377749 feat(client-desktop): Add configuration loading and saving
Introduces a `Config` struct to manage client settings, including server
URL and API key.
This configuration is stored in a TOML file following OS-standard
conventions.
The `directories` crate is used to determine the correct path for the
configuration file across different operating systems.
The `thiserror` crate is used for custom error handling related to
configuration loading and saving.
Added unit tests to verify the configuration loading and saving logic.
2026-04-17 15:35:45 +02:00
Brummel 98d3cd758e feat: Add desktop client and update workspace
Adds the desktop client as a new workspace member and includes its
initial
Cargo.toml and main.rs files. This lays the groundwork for the desktop
application's user interface and integration with the common library.
2026-04-17 15:29:17 +02:00
Brummel a2c1ff49ac Refactor: Use doctate-common for shared logic
Moves shared types and utility functions to a new `doctate-common`
crate.
This includes:
- API key header constant
- AckResponse and AckStatus types
- Timestamp formatting and parsing utilities

This change centralizes common functionality, reducing duplication and
improving maintainability.
The `server` crate now depends on `doctate-common`.
2026-04-17 15:25:11 +02:00
Brummel ed5047a3ff Add doctate-common crate for shared types
This commit introduces a new `doctate-common` crate to the workspace.
This crate will house shared data structures and constants used by both
the `doctate-server` and any future clients.

This refactoring helps to:
- Reduce code duplication.
- Improve maintainability by centralizing common logic.
- Define a clear API contract for server-client communication.

The following modules have been added:
- `ack`: Contains `AckResponse` and `AckStatus` for server responses.
- `constants`: Defines shared constants like API endpoints and multipart
  field names.
- `timestamp`: Provides utilities for handling RFC3339 timestamps,
  including conversions to and from filesystem-safe strings.
2026-04-17 15:17:37 +02:00
Brummel 0a8e66eecd Update dependencies and fix gitignore
This commit updates various dependencies to their latest versions,
ensuring better compatibility and security. It also corrects the
`.gitignore` file to properly exclude the `target/` directory instead of
`server/target/`.
2026-04-17 15:13:27 +02:00
Brummel 91708b5ae1 Update client overview and architecture 2026-04-17 14:45:45 +02:00
Brummel 98f4c1aa9f Refactor: Update project plan documentation
Update the project plan documentation to reflect recent architectural
changes and planned features. This includes clarifying the role of Axum,
the filesystem structure, endpoint definitions, and the management of
prompts.

Key changes:
- Clarified Axum's role as a coordinator handling sequential workers and
  the web interface.
- Adjusted filesystem structure to use user slugs instead of open/done
  directories and introduced a soft-delete marker.
- Updated endpoint definitions to reflect current and planned API
  structures.
- Removed the mention of `prompts.toml` as prompts are now hardcoded,
  with plans for future override capabilities.
- Added detail on marker semantics for file states.
- Refined the description of LLM provider configuration and its impact
  on the UI.
- Introduced user-specific Whisper settings in `users.toml`.
2026-04-17 13:04:52 +02:00
Brummel 997af407b6 feat: Add hunspell dictionary support to gazetteer
Integrates the `spellbook` crate to allow for a Hunspell dictionary to
be used as a veto mechanism within the gazetteer.

This prevents common German words from being incorrectly rewritten to
vocabulary entries if they fall within the edit distance threshold. For
example, "Kaktus" will no longer be rewritten to "Lantus" if a Hunspell
dictionary is provided and contains "Kaktus".

The `hunspell_dict_path` configuration option is added, defaulting to
`/usr/share/hunspell/de_DE`. This path should point to the stem of the
Hunspell dictionary files (e.g., `/usr/share/hunspell/de_DE.aff` and
`/usr/share/hunspell/de_DE.dic`).

The gazetteer now supports an optional `DictChecker` trait, allowing for
pluggable dictionary implementations. `SpellbookDict` is the initial
implementation using the `spellbook` crate.

Includes new tests to verify the dict veto functionality, including
handling of case sensitivity and exact matches. An ignored test is added
for live verification against system-installed Hunspell dictionaries.
2026-04-17 12:30:34 +02:00
Brummel 421fbb3e46 Update system prompt for medical assistant
The system prompt for the medical assistant LLM has been updated to
improve clarity and explicitly state the desired formatting for
corrections and uncertainties. This includes:

- Consolidating similar correction examples.
- Specifying that only "==text==" annotations are allowed for
  corrections and uncertainties.
- Explicitly disallowing other annotation formats like "(unsicher)" or
  "[TODO]".
2026-04-17 11:27:19 +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 13e1950050 Refactor gazetteer to replace post-AI
The gazetteer has been refactored to act as a post-AI normalization
filter. Previously, it was used to annotate LLM input with potential
corrections from a vocabulary. This approach was ineffective because
LLMs often override such hints.

The new approach applies the gazetteer *after* the LLM has generated its
output. This allows for deterministic correction of known terminology,
including fixing LLM output drift (e.g., anglicized drug names).

Key changes:
- `annotate` function renamed to `replace`.
- The output format changes from `Canonical [?original]` to simply the
  `Canonical` form.
- The gazetteer now operates on the final LLM output before persistence,
  ensuring consistency.
- Tests have been updated to reflect this new behavior, focusing on the
  final output rather than the LLM request payload.
2026-04-16 20:21:07 +02:00
Brummel bacbcb90fe Refactor gazetteer annotation format
The gazetteer now annotates text with `Canonical [?original]`. This
prioritizes the corrected term for the LLM while keeping the original
transcription as a fallback.

This change aligns the gazetteer's annotation strategy with the LLM's
prompt, which expects corrections to be marked for review. Previously,
the format was `original [?Canonical]`, which could lead to the LLM
using the potentially incorrect original term.
2026-04-16 20:16:42 +02:00
Brummel 32f6557d85 Remove obsolete vocabulary build scripts
The build scripts for generating `anatomy.txt` and related files have
been removed as they are no longer used or maintained. The Kölner
Phonetik module was also removed as it was not being utilized.
2026-04-16 18:56:08 +02:00
Brummel 17fb14a741 Update hunspell filter to use min token length
The `filter-hunspell-de.sh` script now uses a `MIN_LEN` variable that is
synchronized with the `MIN_TOKEN_LEN` constant in
`src/gazetteer/mod.rs`.
This ensures that short tokens, which are prone to false positives due
to
collisions with common German words, are filtered out by both the
gazetteer
loading and the Hunspell dictionary processing.

Additionally, the script's dependency instructions have been updated to
be
more comprehensive, listing package manager commands for Arch/CachyOS,
Debian/Ubuntu, and Fedora. The `awk` command has been introduced to
filter
out tokens shorter than `MIN_LEN` before sorting and deduplicating.

The `Gazetteer::insert` method has been updated to skip entries shorter
than `MIN_TOKEN_LEN` to prevent noisy phonetic collisions. The
`best_candidate`
helper function has been extracted to improve readability and structure.
The test cases have been updated to reflect the change in
`MIN_TOKEN_LEN`
from 4 to 5 and to use more relevant examples for the updated
functionality,
such as "Cerebrum" and "Zerebrum". The `anatomy.txt` file has been
updated
to reflect that anatomical terms are currently disabled. The
`medications.txt` file has been significantly expanded with a curated
list
of German medication brand and active ingredient names.
2026-04-16 18:48:15 +02:00
Brummel 3da5a36dc8 Add scripts to build anatomy vocabulary 2026-04-16 17:34:38 +02:00
Brummel 8a394ec8c0 Add vocabulary files for server initialization
Adds three new vocabulary files to the server's vocabulary directory:
`anatomy.txt`, `medications.txt`, and `substances.txt`. A README.md is
also added to explain their purpose, format, and population strategy.
The `server/vocab/raw/` directory is added to `.gitignore`.
2026-04-16 17:31:38 +02:00
Brummel e16cb988ed feat: Add gazetteer annotation to user prompt
This commit introduces functionality to annotate the user prompt with
potential proper name corrections from a gazetteer. The LLM will then
use these annotations to improve the accuracy of transcriptions,
especially for medical terms and names.

The `SYSTEM_PROMPT` has also been updated to inform the LLM about these
new annotations and how to handle them.
2026-04-16 17:28:54 +02:00