Fix critical integration issues for end-to-end functionality
- Rewrite SidecarManager as singleton with OnceLock, reusing one Python process across all commands instead of spawning per call - Separate stdin/stdout ownership with dedicated BufReader to prevent data corruption between wait_for_ready and send_and_receive - Add ensure_running() for auto-start on first command - Fix asset protocol URL: use convertFileSrc() instead of manual encodeURIComponent which broke file paths with slashes - Add +layout.svelte with global dark theme, CSS reset, and custom scrollbar styling to prevent white flash on startup - Register AppState with Tauri .manage(), initialize SQLite database on app startup at ~/.voicetonotes/voice_to_notes.db - Wire project commands (create/get/list) to real database queries instead of placeholder stubs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,7 @@
|
|||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
use crate::sidecar::messages::IPCMessage;
|
use crate::sidecar::messages::IPCMessage;
|
||||||
use crate::sidecar::SidecarManager;
|
use crate::sidecar::sidecar;
|
||||||
|
|
||||||
fn get_sidecar() -> Result<SidecarManager, String> {
|
|
||||||
let python_path = std::env::current_dir()
|
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.join("../python")
|
|
||||||
.canonicalize()
|
|
||||||
.map_err(|e| format!("Cannot find python directory: {e}"))?;
|
|
||||||
|
|
||||||
let manager = SidecarManager::new();
|
|
||||||
manager.start(&python_path.to_string_lossy())?;
|
|
||||||
Ok(manager)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a chat message to the AI provider via the Python sidecar.
|
/// Send a chat message to the AI provider via the Python sidecar.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -22,14 +10,8 @@ pub fn ai_chat(
|
|||||||
transcript_context: Option<String>,
|
transcript_context: Option<String>,
|
||||||
provider: Option<String>,
|
provider: Option<String>,
|
||||||
) -> Result<Value, String> {
|
) -> Result<Value, String> {
|
||||||
let manager = get_sidecar()?;
|
let manager = sidecar();
|
||||||
|
manager.ensure_running()?;
|
||||||
let request_id = uuid::Uuid::new_v4().to_string();
|
|
||||||
let payload = json!({
|
|
||||||
"action": "chat",
|
|
||||||
"messages": messages,
|
|
||||||
"transcript_context": transcript_context.unwrap_or_default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// If a specific provider is requested, set it first
|
// If a specific provider is requested, set it first
|
||||||
if let Some(p) = provider {
|
if let Some(p) = provider {
|
||||||
@@ -41,7 +23,17 @@ pub fn ai_chat(
|
|||||||
let _ = manager.send_and_receive(&set_msg)?;
|
let _ = manager.send_and_receive(&set_msg)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = IPCMessage::new(&request_id, "ai.chat", payload);
|
let request_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
let msg = IPCMessage::new(
|
||||||
|
&request_id,
|
||||||
|
"ai.chat",
|
||||||
|
json!({
|
||||||
|
"action": "chat",
|
||||||
|
"messages": messages,
|
||||||
|
"transcript_context": transcript_context.unwrap_or_default(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
let response = manager.send_and_receive(&msg)?;
|
let response = manager.send_and_receive(&msg)?;
|
||||||
|
|
||||||
if response.msg_type == "error" {
|
if response.msg_type == "error" {
|
||||||
@@ -57,7 +49,8 @@ pub fn ai_chat(
|
|||||||
/// List available AI providers.
|
/// List available AI providers.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn ai_list_providers() -> Result<Value, String> {
|
pub fn ai_list_providers() -> Result<Value, String> {
|
||||||
let manager = get_sidecar()?;
|
let manager = sidecar();
|
||||||
|
manager.ensure_running()?;
|
||||||
|
|
||||||
let request_id = uuid::Uuid::new_v4().to_string();
|
let request_id = uuid::Uuid::new_v4().to_string();
|
||||||
let msg = IPCMessage::new(
|
let msg = IPCMessage::new(
|
||||||
@@ -73,7 +66,8 @@ pub fn ai_list_providers() -> Result<Value, String> {
|
|||||||
/// Configure an AI provider with API key/settings.
|
/// Configure an AI provider with API key/settings.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn ai_configure(provider: String, config: Value) -> Result<Value, String> {
|
pub fn ai_configure(provider: String, config: Value) -> Result<Value, String> {
|
||||||
let manager = get_sidecar()?;
|
let manager = sidecar();
|
||||||
|
manager.ensure_running()?;
|
||||||
|
|
||||||
let request_id = uuid::Uuid::new_v4().to_string();
|
let request_id = uuid::Uuid::new_v4().to_string();
|
||||||
let msg = IPCMessage::new(
|
let msg = IPCMessage::new(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
use crate::sidecar::messages::IPCMessage;
|
use crate::sidecar::messages::IPCMessage;
|
||||||
use crate::sidecar::SidecarManager;
|
use crate::sidecar::sidecar;
|
||||||
|
|
||||||
/// Export transcript to caption/text format via the Python sidecar.
|
/// Export transcript to caption/text format via the Python sidecar.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -12,16 +12,8 @@ pub fn export_transcript(
|
|||||||
output_path: String,
|
output_path: String,
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
) -> Result<Value, String> {
|
) -> Result<Value, String> {
|
||||||
let python_path = std::env::current_dir()
|
let manager = sidecar();
|
||||||
.map_err(|e| e.to_string())?
|
manager.ensure_running()?;
|
||||||
.join("../python")
|
|
||||||
.canonicalize()
|
|
||||||
.map_err(|e| format!("Cannot find python directory: {e}"))?;
|
|
||||||
|
|
||||||
let python_path_str = python_path.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
let manager = SidecarManager::new();
|
|
||||||
manager.start(&python_path_str)?;
|
|
||||||
|
|
||||||
let request_id = uuid::Uuid::new_v4().to_string();
|
let request_id = uuid::Uuid::new_v4().to_string();
|
||||||
let msg = IPCMessage::new(
|
let msg = IPCMessage::new(
|
||||||
|
|||||||
@@ -1,27 +1,23 @@
|
|||||||
|
use tauri::State;
|
||||||
|
|
||||||
use crate::db::models::Project;
|
use crate::db::models::Project;
|
||||||
|
use crate::db::queries;
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn create_project(name: String) -> Result<Project, String> {
|
pub fn create_project(name: String, state: State<AppState>) -> Result<Project, String> {
|
||||||
// TODO: Use actual database connection from app state
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||||
Ok(Project {
|
queries::create_project(&conn, &name).map_err(|e| e.to_string())
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
|
||||||
name,
|
|
||||||
created_at: chrono::Utc::now().to_rfc3339(),
|
|
||||||
updated_at: chrono::Utc::now().to_rfc3339(),
|
|
||||||
settings: None,
|
|
||||||
status: "active".to_string(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_project(id: String) -> Result<Option<Project>, String> {
|
pub fn get_project(id: String, state: State<AppState>) -> Result<Option<Project>, String> {
|
||||||
// TODO: Use actual database connection from app state
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||||
let _ = id;
|
queries::get_project(&conn, &id).map_err(|e| e.to_string())
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_projects() -> Result<Vec<Project>, String> {
|
pub fn list_projects(state: State<AppState>) -> Result<Vec<Project>, String> {
|
||||||
// TODO: Use actual database connection from app state
|
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||||
Ok(vec![])
|
queries::list_projects(&conn).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
use crate::sidecar::messages::IPCMessage;
|
use crate::sidecar::messages::IPCMessage;
|
||||||
use crate::sidecar::SidecarManager;
|
use crate::sidecar::sidecar;
|
||||||
|
|
||||||
/// Start transcription of an audio file via the Python sidecar.
|
/// Start transcription of an audio file via the Python sidecar.
|
||||||
///
|
|
||||||
/// This is a blocking command — it starts the sidecar if needed,
|
|
||||||
/// sends the transcribe request, and waits for the result.
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn transcribe_file(
|
pub fn transcribe_file(
|
||||||
file_path: String,
|
file_path: String,
|
||||||
@@ -14,17 +11,8 @@ pub fn transcribe_file(
|
|||||||
device: Option<String>,
|
device: Option<String>,
|
||||||
language: Option<String>,
|
language: Option<String>,
|
||||||
) -> Result<Value, String> {
|
) -> Result<Value, String> {
|
||||||
// Determine Python sidecar path (relative to app)
|
let manager = sidecar();
|
||||||
let python_path = std::env::current_dir()
|
manager.ensure_running()?;
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.join("../python")
|
|
||||||
.canonicalize()
|
|
||||||
.map_err(|e| format!("Cannot find python directory: {e}"))?;
|
|
||||||
|
|
||||||
let python_path_str = python_path.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
let manager = SidecarManager::new();
|
|
||||||
manager.start(&python_path_str)?;
|
|
||||||
|
|
||||||
let request_id = uuid::Uuid::new_v4().to_string();
|
let request_id = uuid::Uuid::new_v4().to_string();
|
||||||
let msg = IPCMessage::new(
|
let msg = IPCMessage::new(
|
||||||
@@ -63,16 +51,8 @@ pub fn run_pipeline(
|
|||||||
max_speakers: Option<u32>,
|
max_speakers: Option<u32>,
|
||||||
skip_diarization: Option<bool>,
|
skip_diarization: Option<bool>,
|
||||||
) -> Result<Value, String> {
|
) -> Result<Value, String> {
|
||||||
let python_path = std::env::current_dir()
|
let manager = sidecar();
|
||||||
.map_err(|e| e.to_string())?
|
manager.ensure_running()?;
|
||||||
.join("../python")
|
|
||||||
.canonicalize()
|
|
||||||
.map_err(|e| format!("Cannot find python directory: {e}"))?;
|
|
||||||
|
|
||||||
let python_path_str = python_path.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
let manager = SidecarManager::new();
|
|
||||||
manager.start(&python_path_str)?;
|
|
||||||
|
|
||||||
let request_id = uuid::Uuid::new_v4().to_string();
|
let request_id = uuid::Uuid::new_v4().to_string();
|
||||||
let msg = IPCMessage::new(
|
let msg = IPCMessage::new(
|
||||||
|
|||||||
@@ -10,12 +10,16 @@ use commands::project::{create_project, get_project, list_projects};
|
|||||||
use commands::settings::{load_settings, save_settings};
|
use commands::settings::{load_settings, save_settings};
|
||||||
use commands::system::{get_data_dir, llama_list_models, llama_start, llama_status, llama_stop};
|
use commands::system::{get_data_dir, llama_list_models, llama_start, llama_status, llama_stop};
|
||||||
use commands::transcribe::{run_pipeline, transcribe_file};
|
use commands::transcribe::{run_pipeline, transcribe_file};
|
||||||
|
use state::AppState;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
|
let app_state = AppState::new().expect("Failed to initialize app state");
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.manage(app_state)
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
create_project,
|
create_project,
|
||||||
get_project,
|
get_project,
|
||||||
|
|||||||
@@ -2,38 +2,82 @@ pub mod ipc;
|
|||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
|
||||||
use std::io::{BufRead, BufReader, Write};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, ChildStdin, Command, Stdio};
|
||||||
use std::sync::Mutex;
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
use crate::sidecar::messages::IPCMessage;
|
use crate::sidecar::messages::IPCMessage;
|
||||||
|
|
||||||
|
/// Get the global sidecar manager singleton.
|
||||||
|
pub fn sidecar() -> &'static SidecarManager {
|
||||||
|
static INSTANCE: OnceLock<SidecarManager> = OnceLock::new();
|
||||||
|
INSTANCE.get_or_init(SidecarManager::new)
|
||||||
|
}
|
||||||
|
|
||||||
/// Manages the Python sidecar process lifecycle.
|
/// Manages the Python sidecar process lifecycle.
|
||||||
|
/// Uses separated stdin/stdout ownership to avoid BufReader conflicts.
|
||||||
pub struct SidecarManager {
|
pub struct SidecarManager {
|
||||||
process: Mutex<Option<Child>>,
|
process: Mutex<Option<Child>>,
|
||||||
|
stdin: Mutex<Option<ChildStdin>>,
|
||||||
|
reader: Mutex<Option<BufReader<std::process::ChildStdout>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SidecarManager {
|
impl SidecarManager {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
process: Mutex::new(None),
|
process: Mutex::new(None),
|
||||||
|
stdin: Mutex::new(None),
|
||||||
|
reader: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure the sidecar is running, starting it if needed.
|
||||||
|
pub fn ensure_running(&self) -> Result<(), String> {
|
||||||
|
if self.is_running() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let python_path = std::env::current_dir()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.join("../python")
|
||||||
|
.canonicalize()
|
||||||
|
.map_err(|e| format!("Cannot find python directory: {e}"))?;
|
||||||
|
|
||||||
|
self.start(&python_path.to_string_lossy())
|
||||||
|
}
|
||||||
|
|
||||||
/// Spawn the Python sidecar process.
|
/// Spawn the Python sidecar process.
|
||||||
pub fn start(&self, python_path: &str) -> Result<(), String> {
|
pub fn start(&self, python_path: &str) -> Result<(), String> {
|
||||||
let child = Command::new("python3")
|
// Stop existing process if any
|
||||||
|
self.stop().ok();
|
||||||
|
|
||||||
|
let mut child = Command::new("python3")
|
||||||
.arg("-m")
|
.arg("-m")
|
||||||
.arg("voice_to_notes.main")
|
.arg("voice_to_notes.main")
|
||||||
.current_dir(python_path)
|
.current_dir(python_path)
|
||||||
.env("PYTHONPATH", python_path)
|
.env("PYTHONPATH", python_path)
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::inherit()) // Let sidecar logs go to parent's stderr
|
.stderr(Stdio::inherit())
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| format!("Failed to start sidecar: {e}"))?;
|
.map_err(|e| format!("Failed to start sidecar: {e}"))?;
|
||||||
|
|
||||||
|
// Take ownership of stdin and stdout separately
|
||||||
|
let stdin = child.stdin.take().ok_or("Failed to get sidecar stdin")?;
|
||||||
|
let stdout = child.stdout.take().ok_or("Failed to get sidecar stdout")?;
|
||||||
|
let buf_reader = BufReader::new(stdout);
|
||||||
|
|
||||||
|
{
|
||||||
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
||||||
*proc = Some(child);
|
*proc = Some(child);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut s = self.stdin.lock().map_err(|e| e.to_string())?;
|
||||||
|
*s = Some(stdin);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut r = self.reader.lock().map_err(|e| e.to_string())?;
|
||||||
|
*r = Some(buf_reader);
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for the "ready" message
|
// Wait for the "ready" message
|
||||||
self.wait_for_ready()?;
|
self.wait_for_ready()?;
|
||||||
@@ -43,35 +87,40 @@ impl SidecarManager {
|
|||||||
|
|
||||||
/// Wait for the sidecar to send its ready message.
|
/// Wait for the sidecar to send its ready message.
|
||||||
fn wait_for_ready(&self) -> Result<(), String> {
|
fn wait_for_ready(&self) -> Result<(), String> {
|
||||||
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
let mut reader_guard = self.reader.lock().map_err(|e| e.to_string())?;
|
||||||
if let Some(ref mut child) = *proc {
|
if let Some(ref mut reader) = *reader_guard {
|
||||||
if let Some(ref mut stdout) = child.stdout {
|
let mut line = String::new();
|
||||||
let reader = BufReader::new(stdout);
|
loop {
|
||||||
for line in reader.lines() {
|
line.clear();
|
||||||
let line = line.map_err(|e| format!("Read error: {e}"))?;
|
let bytes = reader
|
||||||
if line.is_empty() {
|
.read_line(&mut line)
|
||||||
|
.map_err(|e| format!("Read error: {e}"))?;
|
||||||
|
if bytes == 0 {
|
||||||
|
return Err("Sidecar closed stdout before sending ready".to_string());
|
||||||
|
}
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Ok(msg) = serde_json::from_str::<IPCMessage>(&line) {
|
if let Ok(msg) = serde_json::from_str::<IPCMessage>(trimmed) {
|
||||||
if msg.msg_type == "ready" {
|
if msg.msg_type == "ready" {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If we got a non-ready message, something's wrong but don't block forever
|
// Non-ready message: something is wrong
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Err("Sidecar did not send ready message".to_string())
|
Err("Sidecar did not send ready message".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a message to the sidecar and read the response.
|
/// Send a message to the sidecar and read the response.
|
||||||
/// This is a blocking call.
|
/// This is a blocking call.
|
||||||
pub fn send_and_receive(&self, msg: &IPCMessage) -> Result<IPCMessage, String> {
|
pub fn send_and_receive(&self, msg: &IPCMessage) -> Result<IPCMessage, String> {
|
||||||
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
// Write to stdin
|
||||||
if let Some(ref mut child) = *proc {
|
{
|
||||||
// Write message to stdin
|
let mut stdin_guard = self.stdin.lock().map_err(|e| e.to_string())?;
|
||||||
if let Some(ref mut stdin) = child.stdin {
|
if let Some(ref mut stdin) = *stdin_guard {
|
||||||
let json = serde_json::to_string(msg).map_err(|e| e.to_string())?;
|
let json = serde_json::to_string(msg).map_err(|e| e.to_string())?;
|
||||||
stdin
|
stdin
|
||||||
.write_all(json.as_bytes())
|
.write_all(json.as_bytes())
|
||||||
@@ -83,12 +132,13 @@ impl SidecarManager {
|
|||||||
} else {
|
} else {
|
||||||
return Err("Sidecar stdin not available".to_string());
|
return Err("Sidecar stdin not available".to_string());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Read response from stdout
|
// Read from stdout
|
||||||
if let Some(ref mut stdout) = child.stdout {
|
{
|
||||||
let mut reader = BufReader::new(stdout);
|
let mut reader_guard = self.reader.lock().map_err(|e| e.to_string())?;
|
||||||
|
if let Some(ref mut reader) = *reader_guard {
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
// Read lines until we get a response (skip progress messages, collect them)
|
|
||||||
loop {
|
loop {
|
||||||
line.clear();
|
line.clear();
|
||||||
let bytes_read = reader
|
let bytes_read = reader
|
||||||
@@ -101,41 +151,46 @@ impl SidecarManager {
|
|||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let response: IPCMessage =
|
let response: IPCMessage = serde_json::from_str(trimmed)
|
||||||
serde_json::from_str(trimmed).map_err(|e| format!("Parse error: {e}"))?;
|
.map_err(|e| format!("Parse error: {e}"))?;
|
||||||
|
|
||||||
// If it's a progress message, we could emit it as an event
|
// Skip progress messages, return the final result/error
|
||||||
// For now, skip progress and return the final result/error
|
|
||||||
if response.msg_type != "progress" {
|
if response.msg_type != "progress" {
|
||||||
return Ok(response);
|
return Ok(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err("Sidecar stdout not available".to_string());
|
Err("Sidecar stdout not available".to_string())
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Err("Sidecar not running".to_string())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop the sidecar process.
|
/// Stop the sidecar process.
|
||||||
pub fn stop(&self) -> Result<(), String> {
|
pub fn stop(&self) -> Result<(), String> {
|
||||||
|
// Drop stdin to signal EOF
|
||||||
|
{
|
||||||
|
let mut stdin_guard = self.stdin.lock().map_err(|e| e.to_string())?;
|
||||||
|
*stdin_guard = None;
|
||||||
|
}
|
||||||
|
// Drop reader
|
||||||
|
{
|
||||||
|
let mut reader_guard = self.reader.lock().map_err(|e| e.to_string())?;
|
||||||
|
*reader_guard = None;
|
||||||
|
}
|
||||||
|
// Wait for process to exit
|
||||||
|
{
|
||||||
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
||||||
if let Some(ref mut child) = proc.take() {
|
if let Some(ref mut child) = proc.take() {
|
||||||
// Close stdin to signal EOF
|
|
||||||
drop(child.stdin.take());
|
|
||||||
// Wait briefly for clean exit, then kill
|
|
||||||
match child.wait() {
|
match child.wait() {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(_) => {
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
Err(format!("Sidecar did not exit cleanly: {e}"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_running(&self) -> bool {
|
pub fn is_running(&self) -> bool {
|
||||||
let proc = self.process.lock().ok();
|
let proc = self.process.lock().ok();
|
||||||
|
|||||||
@@ -3,17 +3,28 @@ use std::sync::Mutex;
|
|||||||
|
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
|
|
||||||
|
use crate::db;
|
||||||
|
use crate::llama::LlamaManager;
|
||||||
|
|
||||||
/// Shared application state managed by Tauri.
|
/// Shared application state managed by Tauri.
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db: Mutex<Option<Connection>>,
|
pub db: Mutex<Connection>,
|
||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new(data_dir: PathBuf) -> Self {
|
pub fn new() -> Result<Self, String> {
|
||||||
Self {
|
let data_dir = LlamaManager::data_dir();
|
||||||
db: Mutex::new(None),
|
std::fs::create_dir_all(&data_dir)
|
||||||
|
.map_err(|e| format!("Cannot create data dir: {e}"))?;
|
||||||
|
|
||||||
|
let db_path = data_dir.join("voice_to_notes.db");
|
||||||
|
let conn = db::open_database(&db_path)
|
||||||
|
.map_err(|e| format!("Cannot open database: {e}"))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
db: Mutex::new(conn),
|
||||||
data_dir,
|
data_dir,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/routes/+layout.svelte
Normal file
39
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<script>
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(html, body) {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #0a0a23;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||||
|
Ubuntu, Cantarell, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(*, *::before, *::after) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(::-webkit-scrollbar) {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(::-webkit-scrollbar-track) {
|
||||||
|
background: #0a0a23;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(::-webkit-scrollbar-thumb) {
|
||||||
|
background: #2a3a5e;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(::-webkit-scrollbar-thumb:hover) {
|
||||||
|
background: #4a5568;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke, convertFileSrc } from '@tauri-apps/api/core';
|
||||||
import { open, save } from '@tauri-apps/plugin-dialog';
|
import { open, save } from '@tauri-apps/plugin-dialog';
|
||||||
import WaveformPlayer from '$lib/components/WaveformPlayer.svelte';
|
import WaveformPlayer from '$lib/components/WaveformPlayer.svelte';
|
||||||
import TranscriptEditor from '$lib/components/TranscriptEditor.svelte';
|
import TranscriptEditor from '$lib/components/TranscriptEditor.svelte';
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
});
|
});
|
||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
|
|
||||||
// Convert file path to URL for wavesurfer
|
// Convert file path to asset URL for wavesurfer
|
||||||
audioUrl = `asset://localhost/${encodeURIComponent(filePath)}`;
|
audioUrl = convertFileSrc(filePath);
|
||||||
waveformPlayer?.loadAudio(audioUrl);
|
waveformPlayer?.loadAudio(audioUrl);
|
||||||
|
|
||||||
// Start pipeline (transcription + diarization)
|
// Start pipeline (transcription + diarization)
|
||||||
|
|||||||
Reference in New Issue
Block a user