use serde::{Deserialize, Serialize}; use std::fs; use tauri::State; use crate::db::models::Project; use crate::db::queries; use crate::state::AppState; // ── File-based project types ──────────────────────────────────── #[derive(Serialize, Deserialize)] pub struct ProjectFile { pub version: u32, pub name: String, pub audio_file: String, pub created_at: String, pub segments: Vec, pub speakers: Vec, } #[derive(Serialize, Deserialize)] pub struct ProjectFileSegment { pub text: String, pub start_ms: i64, pub end_ms: i64, pub speaker: Option, pub is_edited: bool, pub words: Vec, } #[derive(Serialize, Deserialize)] pub struct ProjectFileWord { pub word: String, pub start_ms: i64, pub end_ms: i64, pub confidence: f64, } #[derive(Serialize, Deserialize)] pub struct ProjectFileSpeaker { pub label: String, pub display_name: Option, pub color: String, } // ── Input types for save_project_transcript ────────────────────── #[derive(Deserialize)] pub struct WordInput { pub word: String, pub start_ms: i64, pub end_ms: i64, pub confidence: f64, } #[derive(Deserialize)] pub struct SegmentInput { pub text: String, pub start_ms: i64, pub end_ms: i64, pub speaker: Option, // speaker label, not id pub words: Vec, } #[derive(Deserialize)] pub struct SpeakerInput { pub label: String, pub color: String, } // ── Output types for load_project_transcript ───────────────────── #[derive(Serialize)] pub struct WordOutput { pub word: String, pub start_ms: i64, pub end_ms: i64, pub confidence: Option, } #[derive(Serialize)] pub struct SegmentOutput { pub id: String, pub text: String, pub start_ms: i64, pub end_ms: i64, pub speaker: Option, // speaker label pub words: Vec, } #[derive(Serialize)] pub struct SpeakerOutput { pub id: String, pub label: String, pub display_name: Option, pub color: Option, } #[derive(Serialize)] pub struct ProjectTranscript { pub file_path: String, pub segments: Vec, pub speakers: Vec, } // ── Commands ───────────────────────────────────────────────────── #[tauri::command] pub fn create_project(name: String, state: State) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; queries::create_project(&conn, &name).map_err(|e| e.to_string()) } #[tauri::command] pub fn get_project(id: String, state: State) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; queries::get_project(&conn, &id).map_err(|e| e.to_string()) } #[tauri::command] pub fn list_projects(state: State) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; queries::list_projects(&conn).map_err(|e| e.to_string()) } #[tauri::command] pub fn delete_project(id: String, state: State) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; queries::delete_project(&conn, &id).map_err(|e| e.to_string()) } #[tauri::command] pub fn update_segment( segment_id: String, new_text: String, state: State, ) -> Result<(), String> { let conn = state.db.lock().map_err(|e| e.to_string())?; queries::update_segment_text(&conn, &segment_id, &new_text).map_err(|e| e.to_string()) } #[tauri::command] pub fn save_project_transcript( project_id: String, file_path: String, segments: Vec, speakers: Vec, state: State, ) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; // 1. Create media file entry let media_file = queries::create_media_file(&conn, &project_id, &file_path).map_err(|e| e.to_string())?; // 2. Create speaker entries and build label -> id map let mut speaker_map = std::collections::HashMap::new(); for speaker_input in &speakers { let speaker = queries::create_speaker( &conn, &project_id, &speaker_input.label, Some(&speaker_input.color), ) .map_err(|e| e.to_string())?; speaker_map.insert(speaker_input.label.clone(), speaker.id); } // 3. Create segments with words for (index, seg_input) in segments.iter().enumerate() { let speaker_id = seg_input .speaker .as_ref() .and_then(|label| speaker_map.get(label)); let segment = queries::create_segment( &conn, &project_id, &media_file.id, speaker_id.map(|s| s.as_str()), seg_input.start_ms, seg_input.end_ms, &seg_input.text, index as i32, ) .map_err(|e| e.to_string())?; // Create words for this segment for (word_index, word_input) in seg_input.words.iter().enumerate() { queries::create_word( &conn, &segment.id, &word_input.word, word_input.start_ms, word_input.end_ms, Some(word_input.confidence), word_index as i32, ) .map_err(|e| e.to_string())?; } } // 4. Return updated project info queries::get_project(&conn, &project_id) .map_err(|e| e.to_string())? .ok_or_else(|| "Project not found".to_string()) } #[tauri::command] pub fn load_project_transcript( project_id: String, state: State, ) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; // 1. Get media files for the project let media_files = queries::get_media_files_for_project(&conn, &project_id).map_err(|e| e.to_string())?; let media_file = match media_files.first() { Some(mf) => mf, None => return Ok(None), }; // 2. Get speakers for the project and build id -> label map let speakers = queries::get_speakers_for_project(&conn, &project_id).map_err(|e| e.to_string())?; let speaker_label_map: std::collections::HashMap = speakers .iter() .map(|s| (s.id.clone(), s.label.clone())) .collect(); // 3. Get segments for the media file let db_segments = queries::get_segments_for_media(&conn, &media_file.id).map_err(|e| e.to_string())?; // 4. Build output segments with nested words let mut segment_outputs = Vec::with_capacity(db_segments.len()); for seg in &db_segments { let words = queries::get_words_for_segment(&conn, &seg.id).map_err(|e| e.to_string())?; let word_outputs: Vec = words .into_iter() .map(|w| WordOutput { word: w.word, start_ms: w.start_ms, end_ms: w.end_ms, confidence: w.confidence, }) .collect(); let speaker_label = seg .speaker_id .as_ref() .and_then(|sid| speaker_label_map.get(sid)) .cloned(); segment_outputs.push(SegmentOutput { id: seg.id.clone(), text: seg.text.clone(), start_ms: seg.start_ms, end_ms: seg.end_ms, speaker: speaker_label, words: word_outputs, }); } // 5. Build speaker outputs let speaker_outputs: Vec = speakers .into_iter() .map(|s| SpeakerOutput { id: s.id, label: s.label, display_name: s.display_name, color: s.color, }) .collect(); Ok(Some(ProjectTranscript { file_path: media_file.file_path.clone(), segments: segment_outputs, speakers: speaker_outputs, })) } // ── File-based project commands ───────────────────────────────── #[tauri::command] pub fn save_project_file(path: String, project: ProjectFile) -> Result<(), String> { let json = serde_json::to_string_pretty(&project).map_err(|e| e.to_string())?; fs::write(&path, json).map_err(|e| format!("Failed to save project: {e}")) } #[tauri::command] pub fn load_project_file(path: String) -> Result { let json = fs::read_to_string(&path).map_err(|e| format!("Failed to read project: {e}"))?; serde_json::from_str(&json).map_err(|e| format!("Failed to parse project: {e}")) }