6 Commits

Author SHA1 Message Date
Gitea Actions
68ae48a771 chore: bump version to 0.2.5 [skip ci] 2026-03-22 03:23:01 +00:00
Claude
4fed9bccb8 Fix sidecar crash: torch circular import under PyInstaller
All checks were successful
Release / Bump version and tag (push) Successful in 4s
Release / Build (Linux) (push) Successful in 7m27s
Release / Build (macOS) (push) Successful in 7m47s
Release / Build (Windows) (push) Successful in 19m25s
- Exclude ctranslate2.converters from PyInstaller bundle — these modules
  import torch at module level causing circular import crashes, and are
  only needed for model conversion (never used at runtime)
- Defer all heavy ML imports to first handler call instead of startup,
  so the sidecar can send its ready message without loading torch/whisper

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 20:22:56 -07:00
Gitea Actions
ef1481b359 chore: bump version to 0.2.4 [skip ci] 2026-03-22 01:37:55 +00:00
Claude
ba3d5c3997 Fix sidecar.log not created: ensure data dir exists
All checks were successful
Release / Bump version and tag (push) Successful in 3s
Release / Build (Linux) (push) Successful in 7m47s
Release / Build (macOS) (push) Successful in 30m20s
Release / Build (Windows) (push) Successful in 36m45s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 18:37:49 -07:00
Gitea Actions
9a6ea84637 chore: bump version to 0.2.3 [skip ci] 2026-03-21 21:46:10 +00:00
Claude
011ff4e178 Fix sidecar crash recovery and Windows console window
All checks were successful
Release / Bump version and tag (push) Successful in 3s
Release / Build (macOS) (push) Successful in 4m40s
Release / Build (Linux) (push) Successful in 7m34s
Release / Build (Windows) (push) Successful in 17m2s
- Fix is_running() to check actual process liveness via try_wait()
  instead of just checking if the handle exists
- Auto-restart sidecar on pipe errors (broken pipe, closed stdout)
  with one retry attempt
- Hide sidecar console window on Windows (CREATE_NO_WINDOW flag)
- Log sidecar stderr to sidecar.log file for crash diagnostics
- Include exit status in error message when sidecar fails to start

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 14:46:01 -07:00
7 changed files with 158 additions and 26 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "voice-to-notes",
"version": "0.2.2",
"version": "0.2.5",
"description": "Desktop app for transcribing audio/video with speaker identification",
"type": "module",
"scripts": {

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "voice-to-notes"
version = "0.2.2"
version = "0.2.5"
description = "Python sidecar for Voice to Notes — transcription, diarization, and AI services"
requires-python = ">=3.11"
license = "MIT"

View File

@@ -33,7 +33,13 @@ a = Analysis(
hookspath=[],
hooksconfig={},
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_private_assemblies=False,
cipher=block_cipher,

View File

@@ -41,11 +41,15 @@ def ping_handler(msg: IPCMessage) -> IPCMessage:
def make_transcribe_handler() -> HandlerFunc:
"""Create a transcription handler with a persistent TranscribeService."""
from voice_to_notes.services.transcribe import TranscribeService, result_to_payload
service = TranscribeService()
service = None
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
result = service.transcribe(
request_id=msg.id,
@@ -66,11 +70,15 @@ def make_transcribe_handler() -> HandlerFunc:
def make_diarize_handler() -> HandlerFunc:
"""Create a diarization handler with a persistent DiarizeService."""
from voice_to_notes.services.diarize import DiarizeService, diarization_to_payload
service = DiarizeService()
service = None
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
result = service.diarize(
request_id=msg.id,
@@ -163,11 +171,15 @@ def make_diarize_download_handler() -> HandlerFunc:
def make_pipeline_handler() -> HandlerFunc:
"""Create a full pipeline handler (transcribe + diarize + merge)."""
from voice_to_notes.services.pipeline import PipelineService, pipeline_result_to_payload
service = PipelineService()
service = None
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
result = service.run(
request_id=msg.id,
@@ -193,11 +205,15 @@ def make_pipeline_handler() -> HandlerFunc:
def make_export_handler() -> HandlerFunc:
"""Create an export handler."""
from voice_to_notes.services.export import ExportService, make_export_request
service = ExportService()
service = None
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)
output_path = service.export(request)
return IPCMessage(
@@ -211,11 +227,14 @@ def make_export_handler() -> HandlerFunc:
def make_ai_chat_handler() -> HandlerFunc:
"""Create an AI chat handler with persistent AIProviderService."""
from voice_to_notes.services.ai_provider import create_default_service
service = create_default_service()
service = None
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
action = payload.get("action", "chat")

View File

@@ -1,6 +1,6 @@
[package]
name = "voice-to-notes"
version = "0.2.2"
version = "0.2.5"
description = "Voice to Notes — desktop transcription with speaker identification"
authors = ["Voice to Notes Contributors"]
license = "MIT"

View File

@@ -6,6 +6,9 @@ use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::{Mutex, OnceLock};
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use crate::sidecar::messages::IPCMessage;
/// Resource directory set by the Tauri app during setup.
@@ -231,10 +234,33 @@ impl SidecarManager {
self.stop().ok();
eprintln!("[sidecar-rs] Starting frozen sidecar: {}", path.display());
let child = Command::new(path)
.stdin(Stdio::piped())
// Log sidecar stderr to a file for diagnostics
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())
.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()
.map_err(|e| format!("Failed to start sidecar binary: {e}"))?;
@@ -300,7 +326,22 @@ impl SidecarManager {
.read_line(&mut line)
.map_err(|e| format!("Read error: {e}"))?;
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();
if trimmed.is_empty() {
@@ -330,11 +371,46 @@ impl SidecarManager {
/// Send a message and receive the response, calling a callback for intermediate messages.
/// 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>(
&self,
msg: &IPCMessage,
on_intermediate: F,
) -> 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
F: Fn(&IPCMessage),
{
@@ -420,8 +496,39 @@ impl SidecarManager {
}
pub fn is_running(&self) -> bool {
let proc = self.process.lock().ok();
proc.map_or(false, |p| p.is_some())
let mut proc = match self.process.lock() {
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;
}
}
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Voice to Notes",
"version": "0.2.2",
"version": "0.2.5",
"identifier": "com.voicetonotes.app",
"build": {
"beforeDevCommand": "npm run dev",