Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68ae48a771 | ||
|
|
4fed9bccb8 | ||
|
|
ef1481b359 | ||
|
|
ba3d5c3997 | ||
|
|
9a6ea84637 | ||
|
|
011ff4e178 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "voice-to-notes",
|
"name": "voice-to-notes",
|
||||||
"version": "0.2.2",
|
"version": "0.2.5",
|
||||||
"description": "Desktop app for transcribing audio/video with speaker identification",
|
"description": "Desktop app for transcribing audio/video with speaker identification",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "voice-to-notes"
|
name = "voice-to-notes"
|
||||||
version = "0.2.2"
|
version = "0.2.5"
|
||||||
description = "Python sidecar for Voice to Notes — transcription, diarization, and AI services"
|
description = "Python sidecar for Voice to Notes — transcription, diarization, and AI services"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ a = Analysis(
|
|||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
excludes=["tkinter", "test", "unittest", "pip", "setuptools"],
|
excludes=[
|
||||||
|
"tkinter", "test", "unittest", "pip", "setuptools",
|
||||||
|
# ctranslate2.converters imports torch at module level and causes
|
||||||
|
# circular import crashes under PyInstaller. These modules are only
|
||||||
|
# needed for model format conversion, never for inference.
|
||||||
|
"ctranslate2.converters",
|
||||||
|
],
|
||||||
win_no_prefer_redirects=False,
|
win_no_prefer_redirects=False,
|
||||||
win_private_assemblies=False,
|
win_private_assemblies=False,
|
||||||
cipher=block_cipher,
|
cipher=block_cipher,
|
||||||
|
|||||||
@@ -41,11 +41,15 @@ def ping_handler(msg: IPCMessage) -> IPCMessage:
|
|||||||
|
|
||||||
def make_transcribe_handler() -> HandlerFunc:
|
def make_transcribe_handler() -> HandlerFunc:
|
||||||
"""Create a transcription handler with a persistent TranscribeService."""
|
"""Create a transcription handler with a persistent TranscribeService."""
|
||||||
from voice_to_notes.services.transcribe import TranscribeService, result_to_payload
|
service = None
|
||||||
|
|
||||||
service = TranscribeService()
|
|
||||||
|
|
||||||
def handler(msg: IPCMessage) -> IPCMessage:
|
def handler(msg: IPCMessage) -> IPCMessage:
|
||||||
|
nonlocal service
|
||||||
|
if service is None:
|
||||||
|
from voice_to_notes.services.transcribe import TranscribeService
|
||||||
|
service = TranscribeService()
|
||||||
|
from voice_to_notes.services.transcribe import result_to_payload
|
||||||
|
|
||||||
payload = msg.payload
|
payload = msg.payload
|
||||||
result = service.transcribe(
|
result = service.transcribe(
|
||||||
request_id=msg.id,
|
request_id=msg.id,
|
||||||
@@ -66,11 +70,15 @@ def make_transcribe_handler() -> HandlerFunc:
|
|||||||
|
|
||||||
def make_diarize_handler() -> HandlerFunc:
|
def make_diarize_handler() -> HandlerFunc:
|
||||||
"""Create a diarization handler with a persistent DiarizeService."""
|
"""Create a diarization handler with a persistent DiarizeService."""
|
||||||
from voice_to_notes.services.diarize import DiarizeService, diarization_to_payload
|
service = None
|
||||||
|
|
||||||
service = DiarizeService()
|
|
||||||
|
|
||||||
def handler(msg: IPCMessage) -> IPCMessage:
|
def handler(msg: IPCMessage) -> IPCMessage:
|
||||||
|
nonlocal service
|
||||||
|
if service is None:
|
||||||
|
from voice_to_notes.services.diarize import DiarizeService
|
||||||
|
service = DiarizeService()
|
||||||
|
from voice_to_notes.services.diarize import diarization_to_payload
|
||||||
|
|
||||||
payload = msg.payload
|
payload = msg.payload
|
||||||
result = service.diarize(
|
result = service.diarize(
|
||||||
request_id=msg.id,
|
request_id=msg.id,
|
||||||
@@ -163,11 +171,15 @@ def make_diarize_download_handler() -> HandlerFunc:
|
|||||||
|
|
||||||
def make_pipeline_handler() -> HandlerFunc:
|
def make_pipeline_handler() -> HandlerFunc:
|
||||||
"""Create a full pipeline handler (transcribe + diarize + merge)."""
|
"""Create a full pipeline handler (transcribe + diarize + merge)."""
|
||||||
from voice_to_notes.services.pipeline import PipelineService, pipeline_result_to_payload
|
service = None
|
||||||
|
|
||||||
service = PipelineService()
|
|
||||||
|
|
||||||
def handler(msg: IPCMessage) -> IPCMessage:
|
def handler(msg: IPCMessage) -> IPCMessage:
|
||||||
|
nonlocal service
|
||||||
|
if service is None:
|
||||||
|
from voice_to_notes.services.pipeline import PipelineService
|
||||||
|
service = PipelineService()
|
||||||
|
from voice_to_notes.services.pipeline import pipeline_result_to_payload
|
||||||
|
|
||||||
payload = msg.payload
|
payload = msg.payload
|
||||||
result = service.run(
|
result = service.run(
|
||||||
request_id=msg.id,
|
request_id=msg.id,
|
||||||
@@ -193,11 +205,15 @@ def make_pipeline_handler() -> HandlerFunc:
|
|||||||
|
|
||||||
def make_export_handler() -> HandlerFunc:
|
def make_export_handler() -> HandlerFunc:
|
||||||
"""Create an export handler."""
|
"""Create an export handler."""
|
||||||
from voice_to_notes.services.export import ExportService, make_export_request
|
service = None
|
||||||
|
|
||||||
service = ExportService()
|
|
||||||
|
|
||||||
def handler(msg: IPCMessage) -> IPCMessage:
|
def handler(msg: IPCMessage) -> IPCMessage:
|
||||||
|
nonlocal service
|
||||||
|
if service is None:
|
||||||
|
from voice_to_notes.services.export import ExportService
|
||||||
|
service = ExportService()
|
||||||
|
from voice_to_notes.services.export import make_export_request
|
||||||
|
|
||||||
request = make_export_request(msg.payload)
|
request = make_export_request(msg.payload)
|
||||||
output_path = service.export(request)
|
output_path = service.export(request)
|
||||||
return IPCMessage(
|
return IPCMessage(
|
||||||
@@ -211,11 +227,14 @@ def make_export_handler() -> HandlerFunc:
|
|||||||
|
|
||||||
def make_ai_chat_handler() -> HandlerFunc:
|
def make_ai_chat_handler() -> HandlerFunc:
|
||||||
"""Create an AI chat handler with persistent AIProviderService."""
|
"""Create an AI chat handler with persistent AIProviderService."""
|
||||||
from voice_to_notes.services.ai_provider import create_default_service
|
service = None
|
||||||
|
|
||||||
service = create_default_service()
|
|
||||||
|
|
||||||
def handler(msg: IPCMessage) -> IPCMessage:
|
def handler(msg: IPCMessage) -> IPCMessage:
|
||||||
|
nonlocal service
|
||||||
|
if service is None:
|
||||||
|
from voice_to_notes.services.ai_provider import create_default_service
|
||||||
|
service = create_default_service()
|
||||||
|
|
||||||
payload = msg.payload
|
payload = msg.payload
|
||||||
action = payload.get("action", "chat")
|
action = payload.get("action", "chat")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "voice-to-notes"
|
name = "voice-to-notes"
|
||||||
version = "0.2.2"
|
version = "0.2.5"
|
||||||
description = "Voice to Notes — desktop transcription with speaker identification"
|
description = "Voice to Notes — desktop transcription with speaker identification"
|
||||||
authors = ["Voice to Notes Contributors"]
|
authors = ["Voice to Notes Contributors"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::{Child, ChildStdin, Command, Stdio};
|
use std::process::{Child, ChildStdin, Command, Stdio};
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
use crate::sidecar::messages::IPCMessage;
|
use crate::sidecar::messages::IPCMessage;
|
||||||
|
|
||||||
/// Resource directory set by the Tauri app during setup.
|
/// Resource directory set by the Tauri app during setup.
|
||||||
@@ -231,10 +234,33 @@ impl SidecarManager {
|
|||||||
self.stop().ok();
|
self.stop().ok();
|
||||||
eprintln!("[sidecar-rs] Starting frozen sidecar: {}", path.display());
|
eprintln!("[sidecar-rs] Starting frozen sidecar: {}", path.display());
|
||||||
|
|
||||||
let child = Command::new(path)
|
// Log sidecar stderr to a file for diagnostics
|
||||||
.stdin(Stdio::piped())
|
let stderr_cfg = if let Some(data_dir) = DATA_DIR.get() {
|
||||||
|
let _ = std::fs::create_dir_all(data_dir);
|
||||||
|
let log_path = data_dir.join("sidecar.log");
|
||||||
|
eprintln!("[sidecar-rs] Sidecar stderr → {}", log_path.display());
|
||||||
|
match std::fs::File::create(&log_path) {
|
||||||
|
Ok(f) => Stdio::from(f),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[sidecar-rs] Failed to create sidecar.log: {e}");
|
||||||
|
Stdio::inherit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("[sidecar-rs] DATA_DIR not set, sidecar stderr will not be logged");
|
||||||
|
Stdio::inherit()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cmd = Command::new(path);
|
||||||
|
cmd.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::inherit())
|
.stderr(stderr_cfg);
|
||||||
|
|
||||||
|
// Hide the console window on Windows (CREATE_NO_WINDOW = 0x08000000)
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
cmd.creation_flags(0x08000000);
|
||||||
|
|
||||||
|
let child = cmd
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| format!("Failed to start sidecar binary: {e}"))?;
|
.map_err(|e| format!("Failed to start sidecar binary: {e}"))?;
|
||||||
|
|
||||||
@@ -300,7 +326,22 @@ impl SidecarManager {
|
|||||||
.read_line(&mut line)
|
.read_line(&mut line)
|
||||||
.map_err(|e| format!("Read error: {e}"))?;
|
.map_err(|e| format!("Read error: {e}"))?;
|
||||||
if bytes == 0 {
|
if bytes == 0 {
|
||||||
return Err("Sidecar closed stdout before sending ready".to_string());
|
// Try to get the exit code for diagnostics
|
||||||
|
let exit_info = {
|
||||||
|
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
||||||
|
if let Some(ref mut child) = *proc {
|
||||||
|
match child.try_wait() {
|
||||||
|
Ok(Some(status)) => format!(" (exit status: {status})"),
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Err(format!(
|
||||||
|
"Sidecar closed stdout before sending ready{exit_info}. \
|
||||||
|
The Python sidecar may have crashed on startup — check app logs for details."
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@@ -330,11 +371,46 @@ impl SidecarManager {
|
|||||||
|
|
||||||
/// Send a message and receive the response, calling a callback for intermediate messages.
|
/// Send a message and receive the response, calling a callback for intermediate messages.
|
||||||
/// Intermediate messages include progress, pipeline.segment, and pipeline.speaker_update.
|
/// Intermediate messages include progress, pipeline.segment, and pipeline.speaker_update.
|
||||||
|
///
|
||||||
|
/// If the sidecar has crashed (broken pipe), automatically restarts it and retries once.
|
||||||
pub fn send_and_receive_with_progress<F>(
|
pub fn send_and_receive_with_progress<F>(
|
||||||
&self,
|
&self,
|
||||||
msg: &IPCMessage,
|
msg: &IPCMessage,
|
||||||
on_intermediate: F,
|
on_intermediate: F,
|
||||||
) -> Result<IPCMessage, String>
|
) -> Result<IPCMessage, String>
|
||||||
|
where
|
||||||
|
F: Fn(&IPCMessage),
|
||||||
|
{
|
||||||
|
match self.send_and_receive_inner(msg, &on_intermediate) {
|
||||||
|
Ok(response) => Ok(response),
|
||||||
|
Err(e)
|
||||||
|
if e.contains("Write error")
|
||||||
|
|| e.contains("closed stdout")
|
||||||
|
|| e.contains("not available") =>
|
||||||
|
{
|
||||||
|
eprintln!("[sidecar-rs] Sidecar communication failed ({e}), restarting...");
|
||||||
|
self.cleanup_handles();
|
||||||
|
// Stop any zombie process
|
||||||
|
{
|
||||||
|
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
||||||
|
if let Some(ref mut child) = proc.take() {
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.ensure_running()?;
|
||||||
|
self.send_and_receive_inner(msg, &on_intermediate)
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inner implementation of send_and_receive.
|
||||||
|
fn send_and_receive_inner<F>(
|
||||||
|
&self,
|
||||||
|
msg: &IPCMessage,
|
||||||
|
on_intermediate: &F,
|
||||||
|
) -> Result<IPCMessage, String>
|
||||||
where
|
where
|
||||||
F: Fn(&IPCMessage),
|
F: Fn(&IPCMessage),
|
||||||
{
|
{
|
||||||
@@ -420,8 +496,39 @@ impl SidecarManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_running(&self) -> bool {
|
pub fn is_running(&self) -> bool {
|
||||||
let proc = self.process.lock().ok();
|
let mut proc = match self.process.lock() {
|
||||||
proc.map_or(false, |p| p.is_some())
|
Ok(p) => p,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
if let Some(ref mut child) = *proc {
|
||||||
|
// Check if the process has exited
|
||||||
|
match child.try_wait() {
|
||||||
|
Ok(Some(_status)) => {
|
||||||
|
// Process has exited — clean up handles
|
||||||
|
eprintln!("[sidecar-rs] Sidecar process has exited");
|
||||||
|
drop(proc);
|
||||||
|
let _ = self.cleanup_handles();
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Ok(None) => true, // Still running
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clean up stdin/stdout/process handles after the sidecar has exited.
|
||||||
|
fn cleanup_handles(&self) {
|
||||||
|
if let Ok(mut s) = self.stdin.lock() {
|
||||||
|
*s = None;
|
||||||
|
}
|
||||||
|
if let Ok(mut r) = self.reader.lock() {
|
||||||
|
*r = None;
|
||||||
|
}
|
||||||
|
if let Ok(mut p) = self.process.lock() {
|
||||||
|
*p = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Voice to Notes",
|
"productName": "Voice to Notes",
|
||||||
"version": "0.2.2",
|
"version": "0.2.5",
|
||||||
"identifier": "com.voicetonotes.app",
|
"identifier": "com.voicetonotes.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user