summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers')
-rw-r--r--makima/src/server/handlers/files.rs230
-rw-r--r--makima/src/server/handlers/listen.rs82
-rw-r--r--makima/src/server/handlers/mod.rs1
3 files changed, 312 insertions, 1 deletions
diff --git a/makima/src/server/handlers/files.rs b/makima/src/server/handlers/files.rs
new file mode 100644
index 0000000..746d66b
--- /dev/null
+++ b/makima/src/server/handlers/files.rs
@@ -0,0 +1,230 @@
+//! HTTP handlers for file CRUD operations.
+
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::IntoResponse,
+ Json,
+};
+use uuid::Uuid;
+
+use crate::db::models::{CreateFileRequest, FileListResponse, FileSummary, UpdateFileRequest};
+use crate::db::repository;
+use crate::server::messages::ApiError;
+use crate::server::state::SharedState;
+
+/// List all files for the current owner.
+#[utoipa::path(
+ get,
+ path = "/api/v1/files",
+ responses(
+ (status = 200, description = "List of files", body = FileListResponse),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ tag = "Files"
+)]
+pub async fn list_files(State(state): State<SharedState>) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::list_files(pool).await {
+ Ok(files) => {
+ let summaries: Vec<FileSummary> = files.into_iter().map(FileSummary::from).collect();
+ let total = summaries.len() as i64;
+ Json(FileListResponse {
+ files: summaries,
+ total,
+ })
+ .into_response()
+ }
+ Err(e) => {
+ tracing::error!("Failed to list files: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Get a single file by ID.
+#[utoipa::path(
+ get,
+ path = "/api/v1/files/{id}",
+ params(
+ ("id" = Uuid, Path, description = "File ID")
+ ),
+ responses(
+ (status = 200, description = "File details", body = crate::db::models::File),
+ (status = 404, description = "File not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ tag = "Files"
+)]
+pub async fn get_file(
+ State(state): State<SharedState>,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::get_file(pool, id).await {
+ Ok(Some(file)) => Json(file).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "File not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to get file {}: {}", id, e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Create a new file.
+#[utoipa::path(
+ post,
+ path = "/api/v1/files",
+ request_body = CreateFileRequest,
+ responses(
+ (status = 201, description = "File created", body = crate::db::models::File),
+ (status = 400, description = "Invalid request", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ tag = "Files"
+)]
+pub async fn create_file(
+ State(state): State<SharedState>,
+ Json(req): Json<CreateFileRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::create_file(pool, req).await {
+ Ok(file) => (StatusCode::CREATED, Json(file)).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to create file: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Update an existing file.
+#[utoipa::path(
+ put,
+ path = "/api/v1/files/{id}",
+ params(
+ ("id" = Uuid, Path, description = "File ID")
+ ),
+ request_body = UpdateFileRequest,
+ responses(
+ (status = 200, description = "File updated", body = crate::db::models::File),
+ (status = 404, description = "File not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ tag = "Files"
+)]
+pub async fn update_file(
+ State(state): State<SharedState>,
+ Path(id): Path<Uuid>,
+ Json(req): Json<UpdateFileRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::update_file(pool, id, req).await {
+ Ok(Some(file)) => Json(file).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "File not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to update file {}: {}", id, e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Delete a file.
+#[utoipa::path(
+ delete,
+ path = "/api/v1/files/{id}",
+ params(
+ ("id" = Uuid, Path, description = "File ID")
+ ),
+ responses(
+ (status = 204, description = "File deleted"),
+ (status = 404, description = "File not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ tag = "Files"
+)]
+pub async fn delete_file(
+ State(state): State<SharedState>,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::delete_file(pool, id).await {
+ Ok(true) => StatusCode::NO_CONTENT.into_response(),
+ Ok(false) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "File not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to delete file {}: {}", id, e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
diff --git a/makima/src/server/handlers/listen.rs b/makima/src/server/handlers/listen.rs
index bf6746c..93062f3 100644
--- a/makima/src/server/handlers/listen.rs
+++ b/makima/src/server/handlers/listen.rs
@@ -9,6 +9,8 @@ use tokio::sync::mpsc;
use uuid::Uuid;
use crate::audio::{resample_and_mixdown, TARGET_CHANNELS, TARGET_SAMPLE_RATE};
+use crate::db::models::{CreateFileRequest, TranscriptEntry, UpdateFileRequest};
+use crate::db::repository;
use crate::listen::{align_speakers, samples_per_chunk, DialogueSegment, TimestampMode};
use crate::server::messages::{
AudioEncoding, ClientMessage, ServerMessage, StartMessage, TranscriptMessage,
@@ -99,6 +101,11 @@ async fn handle_socket(socket: WebSocket, state: SharedState) {
let mut audio_offset: f32 = 0.0; // Time offset from trimmed audio
let mut finalized_segments: Vec<DialogueSegment> = Vec::new();
+ // File persistence state
+ let mut file_id: Option<Uuid> = None;
+ let mut transcript_entries: Vec<TranscriptEntry> = Vec::new();
+ let mut transcript_counter: u32 = 0;
+
// Reset Sortformer state for new session
{
let mut sortformer = state.sortformer.lock().await;
@@ -329,12 +336,52 @@ async fn handle_socket(socket: WebSocket, state: SharedState) {
// Send segments with adjusted timestamps
for seg in &segments {
+ let adjusted_start = seg.start + audio_offset;
let adjusted_end = seg.end + audio_offset;
if adjusted_end > last_sent_end_time {
+ // Create file on first transcript if database is available
+ if file_id.is_none() {
+ if let Some(ref pool) = state.db_pool {
+ match repository::create_file(pool, CreateFileRequest {
+ name: None, // Auto-generated
+ description: None,
+ transcript: vec![],
+ location: None,
+ }).await {
+ Ok(file) => {
+ file_id = Some(file.id);
+ tracing::info!(
+ session_id = %session_id,
+ file_id = %file.id,
+ "Created file for session"
+ );
+ }
+ Err(e) => {
+ tracing::warn!(
+ session_id = %session_id,
+ error = %e,
+ "Failed to create file for session"
+ );
+ }
+ }
+ }
+ }
+
+ // Track transcript entry
+ transcript_counter += 1;
+ transcript_entries.push(TranscriptEntry {
+ id: format!("{}-{}", session_id, transcript_counter),
+ speaker: seg.speaker.clone(),
+ start: adjusted_start,
+ end: adjusted_end,
+ text: seg.text.clone(),
+ is_final: false,
+ });
+
let _ = response_tx
.send(ServerMessage::Transcript(TranscriptMessage {
speaker: seg.speaker.clone(),
- start: seg.start + audio_offset,
+ start: adjusted_start,
end: adjusted_end,
text: seg.text.clone(),
is_final: false,
@@ -399,6 +446,39 @@ async fn handle_socket(socket: WebSocket, state: SharedState) {
}
}
+ // Save final transcript to file if we have one
+ if let Some(fid) = file_id {
+ if let Some(ref pool) = state.db_pool {
+ // Mark all entries as final
+ for entry in &mut transcript_entries {
+ entry.is_final = true;
+ }
+
+ match repository::update_file(pool, fid, UpdateFileRequest {
+ name: None,
+ description: None,
+ transcript: Some(transcript_entries.clone()),
+ }).await {
+ Ok(_) => {
+ tracing::info!(
+ session_id = %session_id,
+ file_id = %fid,
+ transcript_count = transcript_entries.len(),
+ "Saved final transcript to file"
+ );
+ }
+ Err(e) => {
+ tracing::error!(
+ session_id = %session_id,
+ file_id = %fid,
+ error = %e,
+ "Failed to save final transcript to file"
+ );
+ }
+ }
+ }
+ }
+
// Cleanup
drop(response_tx);
let _ = sender_task.await;
diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs
index 94b0384..f249234 100644
--- a/makima/src/server/handlers/mod.rs
+++ b/makima/src/server/handlers/mod.rs
@@ -1,3 +1,4 @@
//! HTTP and WebSocket request handlers.
+pub mod files;
pub mod listen;