Formatting
This commit is contained in:
@@ -84,7 +84,10 @@ pub async fn chat_once(
|
||||
user_content: &str,
|
||||
timeout: Duration,
|
||||
) -> Result<String, LlmError> {
|
||||
let url = format!("{}/v1/chat/completions", settings.base_url.trim_end_matches('/'));
|
||||
let url = format!(
|
||||
"{}/v1/chat/completions",
|
||||
settings.base_url.trim_end_matches('/')
|
||||
);
|
||||
debug!(%url, model = settings.model, "calling llm chat completions");
|
||||
|
||||
let body = ChatRequest {
|
||||
@@ -92,8 +95,14 @@ pub async fn chat_once(
|
||||
temperature: settings.temperature,
|
||||
stream: false,
|
||||
messages: vec![
|
||||
Message { role: "system", content: system_prompt },
|
||||
Message { role: "user", content: user_content },
|
||||
Message {
|
||||
role: "system",
|
||||
content: system_prompt,
|
||||
},
|
||||
Message {
|
||||
role: "user",
|
||||
content: user_content,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -107,7 +116,9 @@ pub async fn chat_once(
|
||||
if !status.is_success() {
|
||||
// Body deliberately dropped — see LlmError docstring.
|
||||
drop(response);
|
||||
return Err(LlmError::Status { status: status.as_u16() });
|
||||
return Err(LlmError::Status {
|
||||
status: status.as_u16(),
|
||||
});
|
||||
}
|
||||
|
||||
let parsed: ChatResponse = response.json().await.map_err(LlmError::Http)?;
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::path::Path;
|
||||
|
||||
use tracing::{info, warn};
|
||||
|
||||
use super::{AnalyzeJob, AnalyzeSender, ANALYSIS_INPUT_FILE, DOCUMENT_FILE};
|
||||
use super::{ANALYSIS_INPUT_FILE, AnalyzeJob, AnalyzeSender, DOCUMENT_FILE};
|
||||
|
||||
/// Walk `data_path/*/` and enqueue every pending analysis for every user.
|
||||
/// Intended to run once at startup; the same primitive backs the per-user
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//! Pipeline:
|
||||
//! raw markdown → html-escape → ==X== → <mark>X</mark> → pulldown-cmark → html
|
||||
|
||||
use pulldown_cmark::{html, Options, Parser};
|
||||
use pulldown_cmark::{Options, Parser, html};
|
||||
|
||||
/// Render analysis Markdown to HTML. Safe to inject into an Askama template
|
||||
/// with `{{ var|safe }}` because every `<`/`>` from the LLM is pre-escaped
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::time::Duration;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use super::{llm, prompt, AnalysisInput, AnalyzeReceiver, ANALYSIS_INPUT_FILE, DOCUMENT_FILE};
|
||||
use super::{ANALYSIS_INPUT_FILE, AnalysisInput, AnalyzeReceiver, DOCUMENT_FILE, llm, prompt};
|
||||
use crate::config::Config;
|
||||
use crate::gazetteer::Gazetteer;
|
||||
use crate::{BusyGuard, WorkerBusy};
|
||||
@@ -20,10 +20,7 @@ pub async fn run(
|
||||
worker_busy: WorkerBusy,
|
||||
vocab: Arc<Gazetteer>,
|
||||
) {
|
||||
info!(
|
||||
vocab_entries = vocab.len(),
|
||||
"Analyze worker started"
|
||||
);
|
||||
info!(vocab_entries = vocab.len(), "Analyze worker started");
|
||||
let timeout = Duration::from_secs(config.llm_timeout_seconds);
|
||||
|
||||
while let Some(job) = rx.recv().await {
|
||||
@@ -144,7 +141,10 @@ async fn process(
|
||||
/// state machine simply sees the job as still pending and retries.
|
||||
async fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
|
||||
let tmp = path.with_extension({
|
||||
let mut ext = path.extension().map(|s| s.to_os_string()).unwrap_or_default();
|
||||
let mut ext = path
|
||||
.extension()
|
||||
.map(|s| s.to_os_string())
|
||||
.unwrap_or_default();
|
||||
ext.push(".tmp");
|
||||
ext
|
||||
});
|
||||
|
||||
+1
-4
@@ -48,10 +48,7 @@ where
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or(AppError::Unauthorized)?;
|
||||
|
||||
let slug = config
|
||||
.api_keys
|
||||
.get(api_key)
|
||||
.ok_or(AppError::Unauthorized)?;
|
||||
let slug = config.api_keys.get(api_key).ok_or(AppError::Unauthorized)?;
|
||||
|
||||
let user = config
|
||||
.users
|
||||
|
||||
@@ -35,7 +35,10 @@ fn main() -> ExitCode {
|
||||
Ok(Mode::PatchFile { slug, file }) => match read_password_twice() {
|
||||
Ok(pw) => match patch_users_toml(&file, &slug, &hash(&pw)) {
|
||||
Ok(()) => {
|
||||
eprintln!("Updated web_password for slug \"{slug}\" in {}", file.display());
|
||||
eprintln!(
|
||||
"Updated web_password for slug \"{slug}\" in {}",
|
||||
file.display()
|
||||
);
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -131,11 +134,8 @@ fn read_password_twice() -> Result<String, String> {
|
||||
/// Patch `web_password` for the `[[user]]` entry whose `slug` matches.
|
||||
/// Preserves comments and formatting via `toml_edit`. Writes atomically.
|
||||
fn patch_users_toml(path: &Path, slug: &str, new_hash: &str) -> Result<(), String> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.map_err(|e| format!("read: {e}"))?;
|
||||
let mut doc: toml_edit::DocumentMut = content
|
||||
.parse()
|
||||
.map_err(|e| format!("parse: {e}"))?;
|
||||
let content = std::fs::read_to_string(path).map_err(|e| format!("read: {e}"))?;
|
||||
let mut doc: toml_edit::DocumentMut = content.parse().map_err(|e| format!("parse: {e}"))?;
|
||||
|
||||
let users = doc
|
||||
.get_mut("user")
|
||||
@@ -158,8 +158,7 @@ fn patch_users_toml(path: &Path, slug: &str, new_hash: &str) -> Result<(), Strin
|
||||
// Atomic write: tmp file in same dir, then rename.
|
||||
let tmp = path.with_extension("toml.tmp");
|
||||
{
|
||||
let mut f = std::fs::File::create(&tmp)
|
||||
.map_err(|e| format!("create tmp: {e}"))?;
|
||||
let mut f = std::fs::File::create(&tmp).map_err(|e| format!("create tmp: {e}"))?;
|
||||
f.write_all(doc.to_string().as_bytes())
|
||||
.map_err(|e| format!("write tmp: {e}"))?;
|
||||
f.sync_all().map_err(|e| format!("sync tmp: {e}"))?;
|
||||
@@ -203,7 +202,10 @@ role = "doctor"
|
||||
patch_users_toml(&p, "dr_a", "$2b$12$NEW").unwrap();
|
||||
let out = std::fs::read_to_string(&p).unwrap();
|
||||
assert!(out.contains(r#"web_password = "$2b$12$NEW""#), "got: {out}");
|
||||
assert!(out.contains(r#"web_password = "$2b$12$KEEP""#), "dr_b changed: {out}");
|
||||
assert!(
|
||||
out.contains(r#"web_password = "$2b$12$KEEP""#),
|
||||
"dr_b changed: {out}"
|
||||
);
|
||||
assert!(out.contains("# header comment"), "comment lost: {out}");
|
||||
std::fs::remove_file(&p).ok();
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ where
|
||||
let Path(s) = Path::<String>::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(|_| AppError::BadRequest("Invalid case_id".into()))?;
|
||||
let id = CaseId::from_str(&s)
|
||||
.map_err(|_| AppError::BadRequest("Invalid case_id".into()))?;
|
||||
let id =
|
||||
CaseId::from_str(&s).map_err(|_| AppError::BadRequest("Invalid case_id".into()))?;
|
||||
Ok(CaseIdPath(id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,8 +216,7 @@ pub fn validate_and_index_users(users: &[User]) -> Result<HashMap<String, String
|
||||
|
||||
/// Read a required environment variable. Panics with a clear message if missing.
|
||||
fn required_env(name: &str) -> String {
|
||||
std::env::var(name)
|
||||
.unwrap_or_else(|_| panic!("Missing required environment variable: {name}"))
|
||||
std::env::var(name).unwrap_or_else(|_| panic!("Missing required environment variable: {name}"))
|
||||
}
|
||||
|
||||
/// Read a required environment variable and parse it. Panics if missing or unparseable.
|
||||
|
||||
@@ -495,10 +495,7 @@ mod tests {
|
||||
#[test]
|
||||
fn dict_veto_allows_nonword_rewrite() {
|
||||
let g = from_entries(&["Cerebrum"]).with_dict(Box::new(HashSetDict::new(&["Kaktus"])));
|
||||
assert_eq!(
|
||||
g.replace("Blutung im Zerebrum."),
|
||||
"Blutung im Cerebrum."
|
||||
);
|
||||
assert_eq!(g.replace("Blutung im Zerebrum."), "Blutung im Cerebrum.");
|
||||
}
|
||||
|
||||
/// The exact-match short-circuit runs *before* any dict lookup.
|
||||
@@ -552,5 +549,4 @@ mod tests {
|
||||
"inflected form must be recognized via affix expansion"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -10,11 +10,11 @@ pub mod routes;
|
||||
pub mod transcribe;
|
||||
pub mod web_session;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use axum::extract::FromRef;
|
||||
use axum::Router;
|
||||
use axum::extract::FromRef;
|
||||
|
||||
use analyze::AnalyzeSender;
|
||||
use config::Config;
|
||||
|
||||
+6
-8
@@ -4,15 +4,15 @@ use tokio::net::TcpListener;
|
||||
use tower_http::limit::RequestBodyLimitLayer;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{info, warn};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use doctate_server::AppState;
|
||||
use doctate_server::analyze;
|
||||
use doctate_server::config::Config;
|
||||
use doctate_server::gazetteer::{Gazetteer, SpellbookDict};
|
||||
use doctate_server::transcribe;
|
||||
use doctate_server::AppState;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -20,8 +20,8 @@ async fn main() {
|
||||
let config = Arc::new(Config::from_env());
|
||||
|
||||
// Logging: stdout + daily rotating log files.
|
||||
let env_filter = EnvFilter::try_new(&config.log_level)
|
||||
.unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
let env_filter =
|
||||
EnvFilter::try_new(&config.log_level).unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
|
||||
let file_appender = tracing_appender::rolling::daily(&config.log_path, "recorder.log");
|
||||
let (file_writer, _guard) = tracing_appender::non_blocking(file_appender);
|
||||
@@ -131,10 +131,8 @@ async fn main() {
|
||||
transcribe::recovery::scan_and_enqueue(&data_path, &tx).await;
|
||||
// Then catch up oneliners for cases that crashed between
|
||||
// transcript-write and oneliner-write in a previous run.
|
||||
transcribe::recovery::regenerate_missing_oneliners(
|
||||
&data_path, &client, &cfg, &vocab,
|
||||
)
|
||||
.await;
|
||||
transcribe::recovery::regenerate_missing_oneliners(&data_path, &client, &cfg, &vocab)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -41,10 +41,7 @@ pub async fn read_delete_marker(case_dir: &Path) -> Option<DeleteMarker> {
|
||||
}
|
||||
|
||||
/// Write the `.deleted` marker as JSON. Overwrites any existing marker.
|
||||
pub async fn write_delete_marker(
|
||||
case_dir: &Path,
|
||||
marker: &DeleteMarker,
|
||||
) -> std::io::Result<()> {
|
||||
pub async fn write_delete_marker(case_dir: &Path, marker: &DeleteMarker) -> std::io::Result<()> {
|
||||
let bytes = serde_json::to_vec(marker)
|
||||
.map_err(|e| std::io::Error::other(format!("serialize delete marker: {e}")))?;
|
||||
tokio::fs::write(case_dir.join(DELETE_MARKER), bytes).await
|
||||
|
||||
@@ -4,16 +4,18 @@ use axum::extract::State;
|
||||
use axum::response::Redirect;
|
||||
use axum_extra::extract::Form;
|
||||
use serde::Deserialize;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::OffsetDateTime;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::analyze::{AnalyzeJob, AnalyzeSender, ANALYSIS_INPUT_FILE, DOCUMENT_FILE};
|
||||
use crate::analyze::{ANALYSIS_INPUT_FILE, AnalyzeJob, AnalyzeSender, DOCUMENT_FILE};
|
||||
use crate::auth::AuthenticatedWebUser;
|
||||
use crate::config::Config;
|
||||
use crate::error::AppError;
|
||||
use crate::paths::{write_delete_marker, DeleteMarker};
|
||||
use crate::routes::case_actions::{build_analysis_input, reset_case_artefacts, write_input_create_new};
|
||||
use crate::paths::{DeleteMarker, write_delete_marker};
|
||||
use crate::routes::case_actions::{
|
||||
build_analysis_input, reset_case_artefacts, write_input_create_new,
|
||||
};
|
||||
use crate::routes::user_web::locate_case;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -37,7 +39,9 @@ pub async fn handle_bulk_action(
|
||||
let user_root = config.data_path.join(&user.slug);
|
||||
|
||||
match form.action.as_str() {
|
||||
"analyze" => bulk_analyze(&config, &analyze_tx, &user.slug, &user_root, &form.case_ids).await,
|
||||
"analyze" => {
|
||||
bulk_analyze(&config, &analyze_tx, &user.slug, &user_root, &form.case_ids).await
|
||||
}
|
||||
"delete" => bulk_delete(&user.slug, &user_root, &form.case_ids).await,
|
||||
"reset" => {
|
||||
if !user.is_admin() {
|
||||
|
||||
@@ -5,21 +5,21 @@ use std::time::SystemTime;
|
||||
use askama::Template;
|
||||
use axum::extract::State;
|
||||
use axum::response::{Html, Redirect};
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::OffsetDateTime;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use doctate_common::timestamp::{filename_stem_to_recorded_at, now_rfc3339};
|
||||
|
||||
use crate::analyze::{
|
||||
AnalysisInput, AnalyzeJob, AnalyzeSender, RecordingInput, ANALYSIS_INPUT_FILE, DOCUMENT_FILE,
|
||||
ANALYSIS_INPUT_FILE, AnalysisInput, AnalyzeJob, AnalyzeSender, DOCUMENT_FILE, RecordingInput,
|
||||
};
|
||||
use crate::auth::AuthenticatedWebUser;
|
||||
use crate::case_id::CaseIdPath;
|
||||
use crate::config::Config;
|
||||
use crate::error::AppError;
|
||||
use crate::paths::{read_delete_marker, write_delete_marker, DeleteMarker, DELETE_MARKER};
|
||||
use crate::paths::{DELETE_MARKER, DeleteMarker, read_delete_marker, write_delete_marker};
|
||||
use crate::routes::user_web::locate_case_or_404;
|
||||
|
||||
#[derive(Template)]
|
||||
@@ -157,17 +157,13 @@ fn compute_last_mtime(m4as: &[(PathBuf, SystemTime)]) -> SystemTime {
|
||||
/// transkribiert" body. Blank transcripts are silently skipped — they
|
||||
/// represent recordings the transcriber classified as silence and must
|
||||
/// not be pushed to the LLM.
|
||||
async fn read_recordings(
|
||||
m4as: &[(PathBuf, SystemTime)],
|
||||
) -> Result<Vec<RecordingInput>, AppError> {
|
||||
async fn read_recordings(m4as: &[(PathBuf, SystemTime)]) -> Result<Vec<RecordingInput>, AppError> {
|
||||
let mut recordings: Vec<RecordingInput> = Vec::new();
|
||||
for (m4a_path, _) in m4as {
|
||||
let transcript_path = m4a_path.with_extension("transcript.txt");
|
||||
let text = tokio::fs::read_to_string(&transcript_path)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
AppError::BadRequest("Nicht alle Aufnahmen sind transkribiert".into())
|
||||
})?;
|
||||
.map_err(|_| AppError::BadRequest("Nicht alle Aufnahmen sind transkribiert".into()))?;
|
||||
|
||||
if text.trim().is_empty() {
|
||||
continue;
|
||||
@@ -240,7 +236,9 @@ pub(crate) async fn write_input_create_new(
|
||||
|
||||
/// Read `case_dir/document.md`. Returns `None` if no document exists.
|
||||
pub(crate) async fn read_document(case_dir: &Path) -> Option<String> {
|
||||
tokio::fs::read_to_string(case_dir.join(DOCUMENT_FILE)).await.ok()
|
||||
tokio::fs::read_to_string(case_dir.join(DOCUMENT_FILE))
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Reset a case to its raw audio: delete all derived artefacts
|
||||
@@ -388,8 +386,7 @@ async fn latest_delete_batch(user_root: &Path) -> Option<(uuid::Uuid, String)> {
|
||||
None => Some(candidate),
|
||||
// Order: deleted_at desc, then batch desc as tie-breaker.
|
||||
Some(ref cur)
|
||||
if candidate.1 > cur.1
|
||||
|| (candidate.1 == cur.1 && candidate.0 > cur.0) =>
|
||||
if candidate.1 > cur.1 || (candidate.1 == cur.1 && candidate.0 > cur.0) =>
|
||||
{
|
||||
Some(candidate)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::Json;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::auth::AuthenticatedUser;
|
||||
use crate::error::AppError;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum::Json;
|
||||
use serde_json::{json, Value};
|
||||
use axum::extract::State;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use askama::Template;
|
||||
use axum::Form;
|
||||
use axum::extract::State;
|
||||
use axum::response::{Html, IntoResponse, Redirect, Response};
|
||||
use axum::Form;
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::Rng;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use serde::Deserialize;
|
||||
use tracing::{info, warn};
|
||||
|
||||
@@ -49,7 +49,10 @@ pub async fn handle_login_submit(
|
||||
if !valid {
|
||||
if user.is_none() {
|
||||
// Dummy verify so timing is similar regardless of slug existence.
|
||||
let _ = bcrypt::verify(&form.password, "$2b$12$invalidinvalidinvalidinvalidinvalidinvalidinvalidinvalidii");
|
||||
let _ = bcrypt::verify(
|
||||
&form.password,
|
||||
"$2b$12$invalidinvalidinvalidinvalidinvalidinvalidinvalidinvalidii",
|
||||
);
|
||||
}
|
||||
warn!(slug = %form.slug, "login failed");
|
||||
return render_login(Some("Login fehlgeschlagen")).map(IntoResponse::into_response);
|
||||
|
||||
@@ -8,8 +8,8 @@ mod upload;
|
||||
pub(crate) mod user_web;
|
||||
pub(crate) mod web;
|
||||
|
||||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
use axum::routing::{get, post};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
@@ -19,7 +19,10 @@ pub fn api_router() -> Router<AppState> {
|
||||
.route("/api/debug/whoami", get(debug::handle_whoami))
|
||||
.route("/api/upload", post(upload::handle_upload))
|
||||
.route("/api/oneliners", get(oneliners::handle_oneliners))
|
||||
.route("/web/login", get(login::handle_login_page).post(login::handle_login_submit))
|
||||
.route(
|
||||
"/web/login",
|
||||
get(login::handle_login_page).post(login::handle_login_submit),
|
||||
)
|
||||
.route("/web/logout", post(login::handle_logout))
|
||||
.route("/web/cases", get(user_web::handle_my_cases))
|
||||
.route("/web/cases/{case_id}", get(user_web::handle_case_detail))
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use axum::extract::Query;
|
||||
use axum::http::{header, HeaderMap, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use axum::extract::Query;
|
||||
use axum::http::{HeaderMap, StatusCode, header};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use doctate_common::oneliners::{OnelinerEntry, OnelinersResponse};
|
||||
use serde::Deserialize;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use axum::extract::{Multipart, State};
|
||||
use axum::Json;
|
||||
use axum::extract::{Multipart, State};
|
||||
use doctate_common::timestamp::recorded_at_to_filename_stem;
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
use tracing::{info, warn};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use askama::Template;
|
||||
use axum::extract::State;
|
||||
use axum::response::Html;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use time::{format_description::well_known::Rfc3339, Date, OffsetDateTime};
|
||||
use time::{Date, OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
|
||||
use crate::analyze::{recovery as analyze_recovery, ANALYSIS_INPUT_FILE, DOCUMENT_FILE};
|
||||
use crate::PipelineState;
|
||||
use crate::analyze::{ANALYSIS_INPUT_FILE, DOCUMENT_FILE, recovery as analyze_recovery};
|
||||
use crate::auth::AuthenticatedWebUser;
|
||||
use crate::case_id::{CaseId, CaseIdPath};
|
||||
use crate::config::Config;
|
||||
use crate::error::AppError;
|
||||
use crate::routes::web::{scan_recordings, RecordingView};
|
||||
use crate::routes::web::{RecordingView, scan_recordings};
|
||||
use crate::transcribe::recovery as transcribe_recovery;
|
||||
use crate::PipelineState;
|
||||
|
||||
struct UserCaseView {
|
||||
case_id: String,
|
||||
@@ -366,9 +366,7 @@ async fn scan_user_cases(config: &Config, slug: &str, worker_busy: bool) -> Vec<
|
||||
/// a valid UUID. Returns the (case_id-string, absolute path) tuple so
|
||||
/// the caller does not need to recompute either. Non-UUID names are
|
||||
/// logged at WARN level; other rejections are silent (common case).
|
||||
async fn filter_valid_case_dir(
|
||||
entry: tokio::fs::DirEntry,
|
||||
) -> Option<(String, std::path::PathBuf)> {
|
||||
async fn filter_valid_case_dir(entry: tokio::fs::DirEntry) -> Option<(String, std::path::PathBuf)> {
|
||||
let case_id = entry.file_name().into_string().ok()?;
|
||||
if uuid::Uuid::parse_str(&case_id).is_err() {
|
||||
warn!(case_id = %case_id, "Skipping non-UUID directory");
|
||||
|
||||
@@ -36,9 +36,7 @@ struct CasesTemplate {
|
||||
cases: Vec<CaseView>,
|
||||
}
|
||||
|
||||
pub async fn handle_case_list(
|
||||
State(config): State<Arc<Config>>,
|
||||
) -> Result<Html<String>, AppError> {
|
||||
pub async fn handle_case_list(State(config): State<Arc<Config>>) -> Result<Html<String>, AppError> {
|
||||
let cases = scan_cases(&config.data_path).await;
|
||||
let template = CasesTemplate { cases };
|
||||
template
|
||||
@@ -82,11 +80,7 @@ fn validate_user_slug(user: &str) -> Result<(), AppError> {
|
||||
|
||||
fn validate_filename(filename: &str) -> Result<(), AppError> {
|
||||
let ok_suffix = filename.ends_with(".m4a") || filename.ends_with(".m4a.failed");
|
||||
if !ok_suffix
|
||||
|| filename.contains('/')
|
||||
|| filename.contains('\\')
|
||||
|| filename.contains("..")
|
||||
{
|
||||
if !ok_suffix || filename.contains('/') || filename.contains('\\') || filename.contains("..") {
|
||||
return Err(AppError::BadRequest("Invalid filename".into()));
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::time::Duration;
|
||||
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use super::{ffmpeg, ollama, whisper, TranscribeReceiver};
|
||||
use super::{TranscribeReceiver, ffmpeg, ollama, whisper};
|
||||
use crate::config::{Config, WhisperUserSettings};
|
||||
use crate::gazetteer::Gazetteer;
|
||||
use crate::{BusyGuard, WorkerBusy};
|
||||
@@ -53,7 +53,15 @@ pub async fn run(
|
||||
}
|
||||
};
|
||||
|
||||
let text = match whisper::transcribe(&client, &config.whisper_url, remuxed.path(), timeout, &settings).await {
|
||||
let text = match whisper::transcribe(
|
||||
&client,
|
||||
&config.whisper_url,
|
||||
remuxed.path(),
|
||||
timeout,
|
||||
&settings,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
error!(audio = %audio_path.display(), error = %e, "whisper call failed");
|
||||
@@ -258,12 +266,9 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn all_transcripts_joined_skips_empty_recordings() {
|
||||
let dir = tempdir().unwrap();
|
||||
tokio::fs::write(
|
||||
dir.path().join("2026-04-16T10-00-00Z.transcript.txt"),
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::fs::write(dir.path().join("2026-04-16T10-00-00Z.transcript.txt"), "")
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::fs::write(
|
||||
dir.path().join("2026-04-16T10-05-00Z.transcript.txt"),
|
||||
"non-empty content",
|
||||
|
||||
Reference in New Issue
Block a user