The LLM client now conditionally adds the `Authorization` header only
when an API key is provided. The `llm_configured` check is updated to
reflect that an API key is not strictly required for Ollama-style
endpoints. A new integration test verifies the functionality against an
Ollama-compatible endpoint without an API key.
Introduce a new "reset" action for bulk operations and a dedicated
endpoint for individual case resets. These features allow administrators
to revert a case to its raw audio state by deleting derived artifacts
like transcripts, analysis input, and documents. The functionality also
includes renaming `.m4a.failed` files back to `.m4a` to re-trigger
transcription.
This commit also:
- Adds a `reset_case_artefacts` helper function to `case_actions.rs`.
- Implements the `handle_reset_case` endpoint.
- Adds the "Reset" button to the UI for administrators.
- Includes unit and integration tests to verify the new functionality
and access control.
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.
The versioning of analysis input and document files
(`analysis_input_v{N}.json`, `document_v{N}.md`) has been removed. All
analysis inputs will now use `analysis_input.json` and generated
documents will use `document.md`.
This simplifies file management, as there's no longer a need to track
and manage multiple versions of these files within a case directory. The
analysis worker will now overwrite the existing `document.md` if it
exists, ensuring that the latest analysis result is always present. The
`version` field has also been removed from `AnalyzeJob` and
`AnalysisInput`.
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.
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.
The analyze recovery scan has been refactored to iterate through user
directories and enqueue pending analysis jobs more efficiently. The
`WorkerBusy` status has been introduced as an `Arc<AtomicBool>` to track
whether the analyze worker is currently processing a job.
This `WorkerBusy` flag is used in the `analyze::worker::run` function
and managed by a `BusyGuard` RAII struct, ensuring the flag is correctly
set and unset even in case of panics.
The `handle_my_cases` and `handle_case_detail` handlers in `user_web.rs`
now utilize the `WorkerBusy` flag to accurately display the "analyzing"
status and to trigger a self-heal of orphaned analysis inputs when the
worker is idle.
Additionally, the `WorkerBusy` type is now exported from
`server/src/lib.rs` to be accessible by other modules. The test cases
have been updated to include the `WorkerBusy` parameter when spawning
the analyze worker.
Introduce a "Back to Cases" button in the document view and update the
case detail page to conditionally display the "Analyze" button. Also,
add a copy-to-clipboard functionality for the document content.
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.
Introduce DeleteMarker struct and associated functions for managing
soft-delete markers within case directories. This enables tracking
deleted cases and supports undo functionality.
feat: Add delete marker support to paths
Introduces a `.deleted` file to mark cases as soft-deleted. The marker
contains a `batch` UUID for grouping deletions and a `deleted_at`
timestamp for ordering.
New functions `is_deleted`, `read_delete_marker`, and
`write_delete_marker` are added to manage this marker. The `locate_case`
function is updated to also consider soft-deleted cases as not found.
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.
Allow overriding the LLM system prompt via the `LLM_SYSTEM_PROMPT`
environment variable. This provides flexibility for customizing LLM
behavior without code changes. A default prompt is used if the
environment variable is unset or empty.
Introduce a new endpoint for reanalyzing cases. This feature allows
administrators
to rebuild analysis input files and enqueue new analysis jobs. The
system will
then generate a new document version, which will become the displayed
document.
Extract the logic for building `AnalysisInput` into a new function
`build_analysis_input`. This function encapsulates the process of
collecting recordings, reading transcripts, and determining the last
recording modification time.
This change improves code organization and reusability by centralizing
the input building logic.
Updates the `LLM_URL` in the project plan and `.env.example` to the
correct value.
Also, adds a warning in `main.rs` to inform the user if the LLM is not
configured, which effectively disables the 'Fall abschließen' feature.
This commit introduces a comprehensive suite of integration tests for
the case closing functionality.
These tests cover various scenarios including:
- Successful case closure with proper file writing and redirection.
- Handling of invalid inputs like non-UUIDs or cases without recordings.
- Authentication and authorization checks, ensuring only the case owner
can close it.
- Error handling for attempting to close a case twice or when the LLM
service is unavailable.
- Verification of the analysis worker's interaction with the mocked LLM.
- Testing the recovery mechanism for re-enqueuing pending analyses.
- Validating the document view displays the latest version.
Introduce a `DocumentTemplate` struct for rendering the document view,
replacing inline HTML.
Introduce a `CaseFlags` struct to encapsulate the logic for determining
various UI states of a case (e.g., `has_document`, `analyzing`,
`can_close`, `llm_missing`).
Update `handle_case_detail` to use `compute_flags` for populating the
`CaseDetailTemplate`.
Update `scan_user_cases` to compute `analyzing` and `has_document` flags
for the `UserCaseView`.
Add new template logic in `case_detail.html` to display actions based on
`CaseFlags`.
Add new template logic in `my_cases.html` to display `analyzing` and
`done-doc` labels for cases.
Create a new `document.html` template.
Add a helper function `any_document_exists` to check for the presence of
a document file.
Adds API endpoints for closing a case, which triggers an analysis job,
and for viewing the generated document.
This also includes:
- New `AppError` variants: `Conflict` and `ServiceUnavailable`.
- Helper functions for file handling, date parsing, and document
searching.
- Unit tests for helper functions.
Introduces a new channel for the analyze pipeline, including its sender
in the `AppState`.
Also updates the test router to create both transcribe and analyze
channels.
This commit introduces the `analyze` module, which orchestrates
communication with Large Language Models (LLMs) for text summarization
and analysis.
Key components include:
- `llm.rs`: Contains the `LlmError` enum and the `chat_once` function
for interacting with OpenAI-compatible LLM APIs.
- `prompt.rs`: Defines the system prompt and logic for rendering user
content from analysis inputs.
- `recovery.rs`: Implements logic to scan for and re-enqueue pending
analysis jobs upon server startup.
- `worker.rs`: The core worker that consumes analysis jobs, processes
them with the LLM, and writes the output.
- `mod.rs`: Defines `AnalyzeJob` and associated channel types for
inter-component communication.
This module enables the server to process dictated recordings, summarize
them using an LLM, and store the results.
Introduced a `test_default` method to `Config` to provide sane defaults
for integration tests. This refactors several test files to use this new
method, reducing boilerplate and improving maintainability.
Added `LLM_TIMEOUT_SECONDS` to the environment variables and `Config`
struct.
This commit introduces several improvements to the `hash-password`
binary:
- **New command-line interface:** Supports printing hashes directly,
hashing via prompts, or patching `web_password` in `users.toml` in
place.
- **Atomic file patching:** Utilizes `toml_edit` to preserve comments,
ordering, and formatting, and writes changes atomically via a
temporary file and rename operation.
- **Robust argument parsing:** Handles various command-line scenarios
and provides informative usage messages.
- **Error handling:** Improves error reporting for failed operations.
- **Added unit tests:** Includes tests for patching behavior and error
conditions.
This commit introduces the foundation for web-based authentication and
user interface. It includes:
- **Session Management**: Securely handling user sessions using
cryptographically generated tokens, cookies with appropriate security
flags, and server-side storage.
- **Login/Logout Functionality**: Endpoints for users to log in with
their credentials and log out, clearing their session.
- **User Interface**: Basic templates for the login page, a list of
cases, and a detailed view of a single case, allowing users to
navigate and view their data.
- **Authorization**: An `AuthenticatedWebUser` extractor to ensure only
logged-in users can access protected web routes.
- **Configuration**: Added a `cookie_secure` option to the configuration
to control the `Secure` flag on session cookies, useful for local
development.
- **Dependencies**: Added necessary dependencies for password hashing,
time handling, and TOML file manipulation.
- **Helper Binary**: Included a `hash-password` binary to simplify
generating bcrypt hashes for user passwords.
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.
This commit introduces the capability to configure user-specific Whisper
settings, including language, hotwords, and initial prompts. These
settings are stored in `users.toml` and are passed to the Whisper
service for more tailored transcription results.
New unit tests have been added to verify that the `transcribe` function
correctly forwards these optional settings to the Whisper client and
omits them when they are not provided.
Additionally, a new directory `tests/fixtures/dictations` has been
created to store M4A audio files, their expected transcripts, and
associated hotword files. This serves as a regression corpus for the
Whisper pipeline. A README file explains the structure and conventions
for adding new fixtures. Several new fixtures have been added, covering
various medical domains and transcription scenarios.
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.
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.
Introduce a new struct `WhisperUserSettings` to hold per-user
transcription settings. This allows users to customize Whisper
transcription for their specific needs directly in the `users.toml`
configuration file.
The changes include:
- Defining `WhisperUserSettings` with optional fields for `language`,
`hotwords`, and `initial_prompt`.
- Integrating `WhisperUserSettings` into the `User` struct, with
`#[serde(default)]` to ensure it defaults to
`WhisperUserSettings::default()` if not present in the TOML.
- Adding new unit tests to verify that users can be parsed correctly
with and without a `[user.whisper]` block.
- Updating existing tests in `auth_test.rs`, `upload_test.rs`, and
`web_test.rs` to include the new `whisper` field, ensuring backward
compatibility.
Update task status to 'done' for several items in the project plan. This
includes setting up the Axum server, filesystem structure,
configuration, logging, and upload endpoints. It also covers the Docker
setup for Axum and faster-whisper, as well as Ollama configuration.
Additionally, the plan now reflects a dedicated `whisper/` service for
faster-whisper, detailing its endpoints, model configuration, hotword
support, and deployment. Changes to the Ollama client configuration and
the oneliner generation logic are also documented.
The transcription queue and recovery mechanisms are marked as done,
along with the faster-whisper and Ollama health checks and retries.
Several aspects of the case management and UI development, including
askama templates and the detail view, have been updated.
Finally, the project plan includes a decision to use `large-v3-turbo`
for the Whisper model and details the implementation of a dedicated
`whisper/` service due to hallucination issues with previous wrappers.
The Ollama model is updated to `gemma4:latest`, and the GPU phase worker
logic is adjusted to leverage the smaller VRAM footprint of the
`large-v3-turbo` model. A new script `scripts/dictate.sh` is introduced
as a stand-in for testing the watch-flow without actual hardware.
This script provides a command-line interface for recording dictations,
uploading them to a server, and retrieving transcripts. It allows for
continuing an existing case, starting a new one, or refreshing the
status of the current case.
The script uses `ffmpeg` for audio recording and `curl` for server
communication. It manages the current case state using a temporary file
`/tmp/doctate-current-case`. Configuration details like `DATA_PATH` and
`API_KEY` are read from the server's `.env` and `users.toml` files.
Hotwords give per-request logit boosts to specified terms throughout
decoding (not just as prefix context like initial_prompt). Measured on
a cardiology dictation: 8/8 correct fachbegriffe vs. 3/8 without.
Off-the-shelf Whisper wrappers hardcode faster-whisper defaults that
cause cascading hallucinations on real dictations (2/13 runs against
ahmetoner v1.9.1). This thin FastAPI service mirrors the /asr interface
but fixes condition_on_previous_text=False, temperature=0.0, and
vad_filter=True so the Axum server can switch by changing WHISPER_URL.
Introduce an `oneliner` field to the `CaseView` struct and update the
`scan_cases` function to read it from `oneliner.txt`. The `cases.html`
template is updated to display the oneliner if present. A new
integration test is added to verify the functionality.
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.
This commit adds several unit tests for the `generate_oneliner`
function, which interacts with the Ollama API. These tests cover:
- Successful parsing of a chat response.
- Normalization of the response content to remove extra whitespace and
quotes.
- Handling of HTTP 500 errors from the server.
- Verification of sent request parameters like `keep_alive` and `model`.
- Parsing errors when the `content` field is missing in the response.
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.
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.
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.