diff --git a/src-tauri/src/commands/audio.rs b/src-tauri/src/commands/audio.rs index 9c3a785..496fbf1 100644 --- a/src-tauri/src/commands/audio.rs +++ b/src-tauri/src/commands/audio.rs @@ -1,17 +1,10 @@ use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicU64, Ordering}; -use tauri::{Emitter, Manager}; -use tokio::sync::{mpsc as tokio_mpsc, Mutex as AsyncMutex}; -use tokio::task::JoinHandle; +use tauri::Manager; use crate::commands::security::ensure_main_window; -use magnotia_audio::{DeviceInfo, MicrophoneCapture, WavWriter}; -use magnotia_core::constants::WHISPER_SAMPLE_RATE; -use magnotia_core::types::AudioSamples; - -const MAX_NATIVE_CAPTURE_RETURN_SAMPLES: usize = WHISPER_SAMPLE_RATE as usize * 60 * 10; +use magnotia_audio::{DeviceInfo, MicrophoneCapture}; /// Enumerate every input device available to cpal, with metadata for the /// Settings device-picker UI. Includes a flag for likely PulseAudio / @@ -25,311 +18,10 @@ pub async fn list_audio_devices(window: tauri::WebviewWindow) -> Result, - join: JoinHandle<()>, -} - -/// Send the stop signal and await full worker termination. Consumes -/// `CaptureWorker` because the contained handles are single-use. -/// Errors from `join.await` (task panicked or was cancelled) are -/// logged and swallowed — the caller only needs the synchronisation -/// barrier, not the worker's return value. -async fn stop_worker(worker: CaptureWorker) { - let _ = worker.stop_tx.send(()).await; - drop(worker.stop_tx); - if let Err(e) = worker.join.await { - eprintln!("[native-capture] worker task did not terminate cleanly: {e}"); - } -} - -/// Shared state for native microphone capture. -pub struct NativeCaptureState { - /// The running accumulator worker, if any. `tokio::sync::Mutex` - /// because the fastest-moving consumer (`stop_worker`) awaits while - /// holding the lock — a `std::sync::Mutex` would have to be released - /// and reacquired around each await point. - worker: AsyncMutex>, - /// Compatibility buffer returned by stop_native_capture. Capped; the - /// authoritative capture is streamed to temp WAV on disk. - all_samples: Arc>>, - wav_writer: Arc>>, - temp_audio_path: Arc>>, - capture_truncated: Arc, -} - -impl NativeCaptureState { - pub fn new() -> Self { - Self { - worker: AsyncMutex::new(None), - all_samples: Arc::new(Mutex::new(Vec::new())), - wav_writer: Arc::new(Mutex::new(None)), - temp_audio_path: Arc::new(Mutex::new(None)), - capture_truncated: Arc::new(AtomicBool::new(false)), - } - } -} - -fn append_recorded_chunk( - all_samples: &Arc>>, - wav_writer: &Arc>>, - truncated: &Arc, - chunk: &[f32], -) { - if let Ok(mut writer) = wav_writer.lock() { - if let Some(writer) = writer.as_mut() { - if let Err(e) = writer.append(chunk) { - eprintln!("[native-capture] temp WAV append failed: {e}"); - } - } - } - - if let Ok(mut all) = all_samples.lock() { - let remaining = MAX_NATIVE_CAPTURE_RETURN_SAMPLES.saturating_sub(all.len()); - if remaining >= chunk.len() { - all.extend_from_slice(chunk); - } else { - if remaining > 0 { - all.extend_from_slice(&chunk[..remaining]); - } - truncated.store(true, Ordering::Relaxed); - } - } -} - -/// Start native microphone capture via cpal. -/// Streams 16kHz mono PCM chunks to the frontend via `native-pcm` events. -/// -/// `device_name`: explicit device name (from `list_audio_devices`) or None / "" -/// to auto-select. The frontend passes `settings.microphoneDevice` here so the -/// user's pick from Settings → Audio → Microphone takes effect. -#[tauri::command] -pub async fn start_native_capture( - window: tauri::WebviewWindow, - app: tauri::AppHandle, - state: tauri::State<'_, NativeCaptureState>, - device_name: Option, -) -> Result<(), String> { - ensure_main_window(&window)?; - eprintln!( - "[native-capture] start_native_capture called (device='{}')", - device_name.as_deref().unwrap_or("") - ); - - // Stop any in-flight worker and AWAIT its termination before opening - // a new capture. Without the join we would race a draining worker - // against the `all_samples.clear()` below, leaving old-session - // samples in the new-session vector (RB-06). - if let Some(existing) = state.worker.lock().await.take() { - stop_worker(existing).await; - } - - // `MicrophoneCapture::start()` is synchronous and may spend up to - // DEVICE_VALIDATION_MS per device per pass (350ms × N devices × 2 passes - // worst case). Run on a blocking thread so the async runtime stays - // responsive to other Tauri commands. (Codex review 2026/04/17 D2) - let device_name_for_blocking = device_name.clone(); - let (capture, rx) = - tokio::task::spawn_blocking(move || match device_name_for_blocking.as_deref() { - Some(name) if !name.is_empty() => MicrophoneCapture::start_with_device(name), - _ => MicrophoneCapture::start(), - }) - .await - .map_err(|e| format!("audio task join error: {e}"))? - .map_err(|e| { - eprintln!("[native-capture] MicrophoneCapture::start failed: {e}"); - e.to_string() - })?; - eprintln!( - "[native-capture] cpal capture started successfully on '{}'", - capture.device_name - ); - - // Wrap capture in Arc so it can be moved into the blocking task - let capture = Arc::new(Mutex::new(Some(capture))); - let capture_clone = capture.clone(); - - let all_samples = state.all_samples.clone(); - all_samples.lock().unwrap().clear(); - state.capture_truncated.store(false, Ordering::Relaxed); - - let temp_path = std::env::temp_dir().join(recording_filename()); - let writer = WavWriter::create(&temp_path, WHISPER_SAMPLE_RATE, 1) - .map_err(|e| format!("Failed to create temp capture WAV: {e}"))?; - *state.wav_writer.lock().unwrap() = Some(writer); - *state.temp_audio_path.lock().unwrap() = Some(temp_path); - - let (stop_tx, mut stop_rx) = tokio_mpsc::channel::<()>(1); - - let all_samples_clone = all_samples.clone(); - let wav_writer_clone = state.wav_writer.clone(); - let truncated_clone = state.capture_truncated.clone(); - - // Spawn a task that reads cpal chunks, downsamples to 16kHz mono, - // and emits events to the frontend. The JoinHandle is retained in - // `state.worker` so `stop_native_capture` can await full termination. - let join = tokio::spawn(async move { - let mut pcm_buffer: Vec = Vec::new(); - let chunk_size = 8000_usize; // ~0.5s at 16kHz - - loop { - // Check for stop signal (non-blocking) - if stop_rx.try_recv().is_ok() { - break; - } - - // Drain available audio chunks from cpal (non-blocking). - // Distinguish Empty (try again) from Disconnected (capture stream - // is dead — exit the loop, don't spin forever). - // (Codex review 2026/04/17 M3) - let mut got_data = false; - let mut capture_dead = false; - loop { - match rx.try_recv() { - Ok(chunk) => { - got_data = true; - let sample_rate = chunk.sample_rate; - let channels = chunk.channels as usize; - - // Downmix to mono if stereo - let mono: Vec = if channels > 1 { - chunk - .samples - .chunks(channels) - .map(|frame| frame.iter().sum::() / channels as f32) - .collect() - } else { - chunk.samples - }; - - // Downsample to 16kHz using simple decimation - // (acceptable quality for speech — same approach as pcm-processor.js) - let ratio = sample_rate as f64 / WHISPER_SAMPLE_RATE as f64; - if (ratio - 1.0).abs() < 0.01 { - pcm_buffer.extend_from_slice(&mono); - } else { - let mut pos: f64 = 0.0; - for &s in &mono { - pos += 1.0; - if pos >= ratio { - pcm_buffer.push(s); - pos -= ratio; - } - } - } - } - Err(std::sync::mpsc::TryRecvError::Empty) => break, - Err(std::sync::mpsc::TryRecvError::Disconnected) => { - eprintln!( - "[native-capture] capture stream disconnected; accumulator exiting" - ); - capture_dead = true; - break; - } - } - } - - // Emit chunks to frontend when we have enough - while pcm_buffer.len() >= chunk_size { - let chunk: Vec = pcm_buffer.drain(..chunk_size).collect(); - - append_recorded_chunk( - &all_samples_clone, - &wav_writer_clone, - &truncated_clone, - &chunk, - ); - - let _ = app.emit( - "native-pcm", - serde_json::json!({ - "samples": chunk, - }), - ); - } - - if capture_dead { - break; - } - if !got_data { - // Avoid busy-spinning when no audio data is available - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } - } - - // Emit any remaining samples - if !pcm_buffer.is_empty() { - append_recorded_chunk( - &all_samples_clone, - &wav_writer_clone, - &truncated_clone, - &pcm_buffer, - ); - let _ = app.emit( - "native-pcm", - serde_json::json!({ - "samples": pcm_buffer, - }), - ); - } - - // Drop the capture to stop the cpal stream - if let Ok(mut cap) = capture_clone.lock() { - cap.take(); - } - if let Ok(mut writer) = wav_writer_clone.lock() { - if let Some(writer) = writer.take() { - if let Err(e) = writer.finalize() { - eprintln!("[native-capture] temp WAV finalize failed: {e}"); - } - } - } - }); - - *state.worker.lock().await = Some(CaptureWorker { stop_tx, join }); - - Ok(()) -} - -/// Stop native microphone capture. Returns all captured samples (16kHz mono). -/// -/// Awaits full worker termination before reading `all_samples`, so the -/// returned vector contains every sample the worker flushed — and -/// nothing from a worker that technically outlived the call (RB-06). -#[tauri::command] -pub async fn stop_native_capture( - window: tauri::WebviewWindow, - state: tauri::State<'_, NativeCaptureState>, -) -> Result, String> { - ensure_main_window(&window)?; - if let Some(worker) = state.worker.lock().await.take() { - stop_worker(worker).await; - } - - let samples = { - let mut all = state.all_samples.lock().unwrap(); - std::mem::take(&mut *all) - }; - if state.capture_truncated.load(Ordering::Relaxed) { - eprintln!( - "[native-capture] stop_native_capture returned a capped compatibility buffer; temp WAV contains the full capture" - ); - } - - Ok(samples) -} - /// Resolve the destination path for a new live-capture recording, -/// ensuring the parent directory exists. Extracted from -/// `persist_audio_samples` so `start_live_transcription_session` can -/// hand the path to the progressive WAV writer before any samples -/// arrive (brief item #19). +/// ensuring the parent directory exists. Used by +/// `start_live_transcription_session` to hand the path to the progressive +/// WAV writer before any samples arrive (brief item #19). pub fn resolve_recording_path( app: &tauri::AppHandle, output_folder: Option<&str>, @@ -368,7 +60,7 @@ fn recording_filename() -> String { .unwrap_or_default(); let secs = duration.as_secs(); let nanos = duration.subsec_nanos(); - let counter = RECORDING_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let counter = RECORDING_COUNTER.fetch_add(1, Ordering::Relaxed); format!("magnotia-{secs}-{nanos:09}-{counter:04}.wav") } @@ -377,14 +69,11 @@ fn recording_filename() -> String { /// restarts, so cross-launch collisions are already impossible — the /// counter is the last-mile guarantee against within-launch same-tick /// collisions. -static RECORDING_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); +static RECORDING_COUNTER: AtomicU64 = AtomicU64::new(0); #[cfg(test)] mod tests { - use super::{recording_filename, stop_worker, CaptureWorker}; - use std::sync::atomic::{AtomicU32, Ordering}; - use std::sync::Arc; - use tokio::sync::mpsc; + use super::recording_filename; #[test] fn recording_filenames_are_unique_across_rapid_calls() { @@ -437,104 +126,4 @@ mod tests { ); assert!(parts[2].chars().all(|c| c.is_ascii_digit())); } - - // RB-06 regression: after `stop_worker(worker).await` completes, the - // underlying task must have exited — no lingering writes to shared - // state can leak past the stop point. The real native-capture - // worker drains a capture queue and appends to `all_samples`; this - // test swaps that for a synthetic worker that bumps an atomic - // counter in a loop and applies a distinct "flush" marker at exit. - // The assertions mirror the real-world invariant a caller needs: - // (a) after stop_worker returns, the worker has run its flush; - // (b) subsequent sleeps see the counter frozen — no writes occur - // after the join barrier. - // Pre-fix behaviour (fire-and-forget `tokio::spawn`) failed both: - // a start→stop→start cycle could observe tail writes from the - // previous worker in the new session's vector. - #[tokio::test] - async fn stop_worker_awaits_full_termination_no_writes_after_join() { - let counter = Arc::new(AtomicU32::new(0)); - let counter_task = counter.clone(); - let (stop_tx, mut stop_rx) = mpsc::channel::<()>(1); - - let join = tokio::spawn(async move { - loop { - if stop_rx.try_recv().is_ok() { - break; - } - counter_task.fetch_add(1, Ordering::SeqCst); - tokio::time::sleep(std::time::Duration::from_millis(1)).await; - } - // Flush marker — mirrors the final pcm_buffer drain in the - // real worker. Setting a value with a distinctive high bit - // so the test can prove the flush ran. - counter_task.fetch_or(0x8000_0000, Ordering::SeqCst); - }); - - // Let the worker accumulate a few bumps before we signal stop. - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - - stop_worker(CaptureWorker { stop_tx, join }).await; - - let after_stop = counter.load(Ordering::SeqCst); - assert!( - after_stop & 0x8000_0000 != 0, - "flush marker must be set post-stop (got {after_stop:#x})" - ); - - // Post-join, no further writes are possible because the task - // has ended. Sleep briefly and re-read to confirm. - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - let later = counter.load(Ordering::SeqCst); - assert_eq!( - later, after_stop, - "no writes must happen after stop_worker returns" - ); - } - - #[tokio::test] - async fn stop_worker_is_idempotent_on_a_worker_that_has_already_exited() { - // A worker that stops itself (channel disconnected, capture - // dead, etc.) must still be join-able cleanly by stop_worker — - // the helper should swallow any expected "task already done" - // condition without panicking. - let (stop_tx, _stop_rx) = mpsc::channel::<()>(1); - let join = tokio::spawn(async { /* exit immediately */ }); - - // Give the task a tick to finish. - tokio::time::sleep(std::time::Duration::from_millis(5)).await; - - // This must not hang or panic. - stop_worker(CaptureWorker { stop_tx, join }).await; - } -} - -pub async fn persist_audio_samples( - app: &tauri::AppHandle, - samples: Vec, - output_folder: Option, -) -> Result { - let path = resolve_recording_path(app, output_folder.as_deref())?; - let path_clone = path.clone(); - - tokio::task::spawn_blocking(move || { - let audio = AudioSamples::mono_16khz(samples); - magnotia_audio::write_wav(&path_clone, &audio).map_err(|e| e.to_string()) - }) - .await - .map_err(|e| e.to_string())??; - - Ok(path.to_string_lossy().to_string()) -} - -/// Save PCM f32 samples as a WAV file. Returns the file path. -#[tauri::command] -pub async fn save_audio( - window: tauri::WebviewWindow, - app: tauri::AppHandle, - samples: Vec, - output_folder: Option, -) -> Result { - ensure_main_window(&window)?; - persist_audio_samples(&app, samples, output_folder).await } diff --git a/src-tauri/src/commands/feedback.rs b/src-tauri/src/commands/feedback.rs index 483e3d5..d99cb27 100644 --- a/src-tauri/src/commands/feedback.rs +++ b/src-tauri/src/commands/feedback.rs @@ -3,11 +3,10 @@ // on AI-generated output feeds a few-shot loop that conditions future // prompts on the user's preferred style. -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use magnotia_storage::{ - list_feedback_examples as db_list_feedback_examples, record_feedback as db_record_feedback, - FeedbackRow, FeedbackTargetType, RecordFeedbackParams, + record_feedback as db_record_feedback, FeedbackTargetType, RecordFeedbackParams, }; use crate::AppState; @@ -35,36 +34,6 @@ pub struct RecordFeedbackInput { pub profile_id: Option, } -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct FeedbackDto { - pub id: i64, - pub target_type: String, - pub target_id: Option, - pub rating: i64, - pub original_text: Option, - pub corrected_text: Option, - pub context_json: Option, - pub profile_id: String, - pub created_at: String, -} - -impl From for FeedbackDto { - fn from(r: FeedbackRow) -> Self { - Self { - id: r.id, - target_type: r.target_type, - target_id: r.target_id, - rating: r.rating, - original_text: r.original_text, - corrected_text: r.corrected_text, - context_json: r.context_json, - profile_id: r.profile_id, - created_at: r.created_at, - } - } -} - fn parse_target_type(raw: &str) -> Result { FeedbackTargetType::parse(raw).ok_or_else(|| format!("unknown feedback target_type: {raw}")) } @@ -91,20 +60,3 @@ pub async fn record_feedback( .map_err(|e| e.to_string()) } -#[tauri::command] -pub async fn list_feedback_examples_cmd( - state: tauri::State<'_, AppState>, - target_type: String, - limit: Option, - min_rating: Option, - profile_id: Option, -) -> Result, String> { - let target = parse_target_type(&target_type)?; - let limit = limit.unwrap_or(8).clamp(1, 64); - let min_rating = min_rating.unwrap_or(0).clamp(-1, 1); - let rows = - db_list_feedback_examples(&state.db, target, limit, min_rating, profile_id.as_deref()) - .await - .map_err(|e| e.to_string())?; - Ok(rows.into_iter().map(FeedbackDto::from).collect()) -} diff --git a/src-tauri/src/commands/models.rs b/src-tauri/src/commands/models.rs index 3d9b879..a23b9b6 100644 --- a/src-tauri/src/commands/models.rs +++ b/src-tauri/src/commands/models.rs @@ -529,12 +529,6 @@ pub async fn download_model( Ok(format!("Model {} downloaded", size)) } -#[tauri::command] -pub fn check_model(size: String) -> Result { - let id = whisper_model_id(&size); - Ok(model_manager::is_downloaded(&id)) -} - #[tauri::command] pub fn list_models() -> Result, String> { let ids = model_manager::list_downloaded(); diff --git a/src-tauri/src/commands/profiles.rs b/src-tauri/src/commands/profiles.rs index 7b4b105..e922a65 100644 --- a/src-tauri/src/commands/profiles.rs +++ b/src-tauri/src/commands/profiles.rs @@ -15,9 +15,8 @@ use magnotia_ai_formatting::extract_corrections; use magnotia_storage::{ add_profile_term as db_add_profile_term, create_profile as db_create_profile, delete_profile as db_delete_profile, delete_profile_term as db_delete_profile_term, - get_profile as db_get_profile, list_profile_terms as db_list_profile_terms, - list_profiles as db_list_profiles, update_profile as db_update_profile, ProfileRow, - ProfileTermRow, + list_profile_terms as db_list_profile_terms, list_profiles as db_list_profiles, + update_profile as db_update_profile, ProfileRow, ProfileTermRow, }; use crate::AppState; @@ -79,17 +78,6 @@ pub async fn list_profiles_cmd( .map_err(|e| e.to_string()) } -#[tauri::command] -pub async fn get_profile_cmd( - state: tauri::State<'_, AppState>, - id: String, -) -> Result, String> { - db_get_profile(&state.db, &id) - .await - .map(|opt| opt.map(ProfileDto::from)) - .map_err(|e| e.to_string()) -} - #[tauri::command] pub async fn create_profile_cmd( state: tauri::State<'_, AppState>, diff --git a/src-tauri/src/commands/transcription.rs b/src-tauri/src/commands/transcription.rs index afd0b6a..d64cb13 100644 --- a/src-tauri/src/commands/transcription.rs +++ b/src-tauri/src/commands/transcription.rs @@ -5,8 +5,6 @@ use std::path::Path; use std::sync::Arc; -use tauri::Emitter; - use crate::commands::build_initial_prompt; use crate::commands::models::{default_model_id_for_engine, ensure_model_loaded}; use crate::commands::security::ensure_main_window; @@ -137,96 +135,6 @@ fn transcribe_samples_sync( }) } -/// Transcribe raw PCM f32 samples (Whisper). Emits "transcription-result" event. -#[tauri::command] -pub async fn transcribe_pcm( - window: tauri::WebviewWindow, - state: tauri::State<'_, AppState>, - app: tauri::AppHandle, - samples: Vec, - chunk_id: u32, - language: String, - initial_prompt: String, - remove_fillers: bool, - british_english: bool, - anti_hallucination: bool, - format_mode: String, - profile_id: Option, -) -> Result<(), String> { - ensure_main_window(&window)?; - if !state.whisper_engine.is_loaded() { - return Err( - "Whisper model not loaded. Download a Whisper model in Settings.".to_string(), - ); - } - let resolved_profile_id = - profile_id.unwrap_or_else(|| magnotia_storage::DEFAULT_PROFILE_ID.to_string()); - - let profile = magnotia_storage::database::get_profile(&state.db, &resolved_profile_id) - .await - .map_err(|e| e.to_string())? - .ok_or_else(|| format!("Profile {resolved_profile_id} not found"))?; - - let profile_terms: Vec = - magnotia_storage::database::list_profile_terms(&state.db, &resolved_profile_id) - .await - .map_err(|e| e.to_string())? - .into_iter() - .map(|t| t.term) - .collect(); - - let engine = state.whisper_engine.clone(); - let options = TranscriptionOptions { - language: Some(language), - initial_prompt: build_initial_prompt( - &initial_prompt, - &profile.initial_prompt, - &profile_terms, - ), - }; - - let timed = tokio::task::spawn_blocking(move || { - let audio = AudioSamples::mono_16khz(samples); - engine - .transcribe_sync(&audio, &options) - .map_err(|e| e.to_string()) - }) - .await - .map_err(|e| e.to_string())??; - - let dictionary_terms = profile_terms.clone(); - - let mut segments: Vec = timed.transcript.segments().to_vec(); - let raw_text = join_segment_text(&segments); - post_process_segments( - &mut segments, - &PostProcessOptions { - remove_fillers, - british_english, - anti_hallucination, - format_mode: FormatMode::parse(&format_mode), - dictionary_terms, - }, - Some(state.llm_engine.as_ref()), - ); - - app.emit( - "transcription-result", - serde_json::json!({ - "status": "transcription", - "segments": segments, - "language": timed.transcript.language(), - "duration": timed.transcript.duration(), - "chunk_id": chunk_id, - "inference_ms": timed.inference_ms, - "raw_text": raw_text, - }), - ) - .map_err(|e| format!("Failed to emit result: {e}"))?; - - Ok(()) -} - fn join_segment_text(segments: &[Segment]) -> String { segments .iter() @@ -342,82 +250,3 @@ pub async fn transcribe_file( })) } -/// Transcribe raw PCM f32 samples (Parakeet). Emits "transcription-result" event. -#[tauri::command] -pub async fn transcribe_pcm_parakeet( - window: tauri::WebviewWindow, - state: tauri::State<'_, AppState>, - app: tauri::AppHandle, - samples: Vec, - chunk_id: u32, - remove_fillers: bool, - british_english: bool, - anti_hallucination: bool, - format_mode: String, - profile_id: Option, -) -> Result<(), String> { - ensure_main_window(&window)?; - if !state.parakeet_engine.is_loaded() { - return Err( - "Parakeet model not loaded. Enable Parakeet in Settings.".to_string(), - ); - } - let resolved_profile_id = - profile_id.unwrap_or_else(|| magnotia_storage::DEFAULT_PROFILE_ID.to_string()); - - // Validate the profile exists so parakeet and whisper behave identically - // when a bogus id slips through from the frontend. - magnotia_storage::database::get_profile(&state.db, &resolved_profile_id) - .await - .map_err(|e| e.to_string())? - .ok_or_else(|| format!("Profile {resolved_profile_id} not found"))?; - - let profile_terms: Vec = - magnotia_storage::database::list_profile_terms(&state.db, &resolved_profile_id) - .await - .map_err(|e| e.to_string())? - .into_iter() - .map(|t| t.term) - .collect(); - - let engine = state.parakeet_engine.clone(); - let options = TranscriptionOptions::default(); - - let timed = tokio::task::spawn_blocking(move || { - transcribe_samples_sync(engine, "parakeet", samples, options) - }) - .await - .map_err(|e| e.to_string())??; - - let dictionary_terms = profile_terms.clone(); - - let mut segments: Vec = timed.transcript.segments().to_vec(); - let raw_text = join_segment_text(&segments); - post_process_segments( - &mut segments, - &PostProcessOptions { - remove_fillers, - british_english, - anti_hallucination, - format_mode: FormatMode::parse(&format_mode), - dictionary_terms, - }, - Some(state.llm_engine.as_ref()), - ); - - app.emit( - "transcription-result", - serde_json::json!({ - "status": "transcription", - "segments": segments, - "language": timed.transcript.language(), - "duration": timed.transcript.duration(), - "chunk_id": chunk_id, - "inference_ms": timed.inference_ms, - "raw_text": raw_text, - }), - ) - .map_err(|e| format!("Failed to emit result: {e}"))?; - - Ok(()) -} diff --git a/src-tauri/src/commands/transcripts.rs b/src-tauri/src/commands/transcripts.rs index f2f61af..16d00fa 100644 --- a/src-tauri/src/commands/transcripts.rs +++ b/src-tauri/src/commands/transcripts.rs @@ -14,11 +14,10 @@ use serde::{Deserialize, Serialize}; use magnotia_storage::{ - count_transcripts, delete_transcript as db_delete_transcript, - get_transcript as db_get_transcript, insert_transcript as db_insert_transcript, - list_transcripts_paged, search_transcripts as db_search_transcripts, - update_transcript as db_update_transcript, update_transcript_meta as db_update_transcript_meta, - InsertTranscriptParams, TranscriptRow, + delete_transcript as db_delete_transcript, get_transcript as db_get_transcript, + insert_transcript as db_insert_transcript, list_transcripts_paged, + search_transcripts as db_search_transcripts, update_transcript as db_update_transcript, + update_transcript_meta as db_update_transcript_meta, InsertTranscriptParams, TranscriptRow, }; use crate::AppState; @@ -145,14 +144,6 @@ pub async fn list_transcripts( .map_err(|e| e.to_string()) } -/// Total count of transcripts (for "showing X of N" UI). -#[tauri::command] -pub async fn count_transcripts_command(state: tauri::State<'_, AppState>) -> Result { - count_transcripts(&state.db) - .await - .map_err(|e| e.to_string()) -} - #[tauri::command] pub async fn get_transcript( state: tauri::State<'_, AppState>, diff --git a/src-tauri/src/commands/update.rs b/src-tauri/src/commands/update.rs index 1dac8a2..1b33867 100644 --- a/src-tauri/src/commands/update.rs +++ b/src-tauri/src/commands/update.rs @@ -8,9 +8,3 @@ pub async fn check_for_update(window: tauri::WebviewWindow) -> Result Result<(), String> { - ensure_main_window(&window)?; - Err("Updates are disabled until release signing is configured.".to_string()) -} diff --git a/src-tauri/src/commands/windows.rs b/src-tauri/src/commands/windows.rs index 91477f7..143b2c9 100644 --- a/src-tauri/src/commands/windows.rs +++ b/src-tauri/src/commands/windows.rs @@ -27,12 +27,6 @@ pub async fn open_preview_window(_app: tauri::AppHandle) -> Result<(), String> { Err(ANDROID_MULTIWINDOW_ERR.into()) } -#[cfg(target_os = "android")] -#[tauri::command] -pub async fn close_preview_window(_app: tauri::AppHandle) -> Result<(), String> { - Err(ANDROID_MULTIWINDOW_ERR.into()) -} - #[cfg(target_os = "android")] #[tauri::command] pub async fn open_viewer_window(_app: tauri::AppHandle) -> Result<(), String> { @@ -152,17 +146,6 @@ pub async fn open_preview_window(app: tauri::AppHandle) -> Result<(), String> { Ok(()) } -/// Hide the transcription preview window without destroying it so the next -/// open is instant. Returns Ok even when no preview window exists. -#[cfg(not(target_os = "android"))] -#[tauri::command] -pub async fn close_preview_window(app: tauri::AppHandle) -> Result<(), String> { - if let Some(window) = app.get_webview_window("transcription-preview") { - window.hide().map_err(|e| e.to_string())?; - } - Ok(()) -} - /// Open the transcript viewer window. #[cfg(not(target_os = "android"))] #[tauri::command] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b0a8f1b..7fe5818 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -294,7 +294,6 @@ pub fn run() { app.manage(PreferencesScript(init_script)); app.manage(commands::hotkey::HotkeyState::new()); - app.manage(commands::audio::NativeCaptureState::new()); app.manage(commands::live::LiveTranscriptionState::default()); app.manage(commands::tts::TtsState::new()); app.manage(commands::meeting::MeetingState::new()); @@ -323,7 +322,6 @@ pub fn run() { save_preferences, // Whisper model management commands::models::download_model, - commands::models::check_model, commands::models::list_models, commands::models::load_model, commands::models::prewarm_default_model_cmd, @@ -346,13 +344,8 @@ pub fn run() { commands::models::load_parakeet_model, commands::models::check_parakeet_engine, // Transcription - commands::transcription::transcribe_pcm, commands::transcription::transcribe_file, - commands::transcription::transcribe_pcm_parakeet, // Audio - commands::audio::save_audio, - commands::audio::start_native_capture, - commands::audio::stop_native_capture, commands::audio::list_audio_devices, // Tasks (canonical SQLite-backed task CRUD) commands::tasks::create_task_cmd, @@ -369,7 +362,6 @@ pub fn run() { commands::tasks::list_recent_completions_cmd, // HITL feedback (Phase 2 roadmap) commands::feedback::record_feedback, - commands::feedback::list_feedback_examples_cmd, // Read aloud (Phase 4 roadmap) commands::tts::tts_speak, commands::tts::tts_stop, @@ -387,7 +379,6 @@ pub fn run() { commands::intentions::delete_implementation_rule, // Profiles + profile terms (canonical SQLite-backed profile CRUD) — Task 12 commands::profiles::list_profiles_cmd, - commands::profiles::get_profile_cmd, commands::profiles::create_profile_cmd, commands::profiles::update_profile_cmd, commands::profiles::delete_profile_cmd, @@ -398,7 +389,6 @@ pub fn run() { // Transcripts (canonical SQLite-backed history) — Day 4 commands::transcripts::add_transcript, commands::transcripts::list_transcripts, - commands::transcripts::count_transcripts_command, commands::transcripts::get_transcript, commands::transcripts::update_transcript, commands::transcripts::update_transcript_meta_cmd, @@ -417,7 +407,6 @@ pub fn run() { commands::windows::open_task_window, commands::windows::open_viewer_window, commands::windows::open_preview_window, - commands::windows::close_preview_window, // Clipboard commands::clipboard::copy_to_clipboard, // Filesystem (Phase 9 save-dialog path) @@ -441,7 +430,6 @@ pub fn run() { commands::hotkey::stop_evdev_hotkey, // Updater commands::update::check_for_update, - commands::update::install_update, ]) .run(tauri::generate_context!()) .expect("error while running Magnotia");