- PyInstaller frozen sidecar: spec file, build script, and ffmpeg path resolver for self-contained distribution without Python prerequisites - Dual-mode sidecar launcher: frozen binary (production) with dev mode fallback - Parallel transcription + diarization pipeline (~30-40% faster) - GPU auto-detection for diarization (CUDA when available) - Async run_pipeline command for real-time progress event delivery - Web Audio API backend for instant playback and seeking - OpenAI-compatible provider replacing LiteLLM client-side routing - Cross-platform RAM detection (Linux/macOS/Windows) - Settings: speaker count hint, token reveal toggles, dark dropdown styling - Loading splash screen, flexbox layout fix for viewport overflow - Gitea Actions CI/CD pipeline (Linux, Windows, macOS ARM) - Updated README and CLAUDE.md documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
140 lines
4.1 KiB
Rust
140 lines
4.1 KiB
Rust
use serde_json::{json, Value};
|
|
use tauri::{AppHandle, Emitter};
|
|
|
|
use crate::sidecar::messages::IPCMessage;
|
|
use crate::sidecar::sidecar;
|
|
|
|
/// Start transcription of an audio file via the Python sidecar.
|
|
#[tauri::command]
|
|
pub fn transcribe_file(
|
|
file_path: String,
|
|
model: Option<String>,
|
|
device: Option<String>,
|
|
language: Option<String>,
|
|
) -> Result<Value, String> {
|
|
let manager = sidecar();
|
|
manager.ensure_running()?;
|
|
|
|
let request_id = uuid::Uuid::new_v4().to_string();
|
|
let msg = IPCMessage::new(
|
|
&request_id,
|
|
"transcribe.start",
|
|
json!({
|
|
"file": file_path,
|
|
"model": model.unwrap_or_else(|| "base".to_string()),
|
|
"device": device.unwrap_or_else(|| "cpu".to_string()),
|
|
"compute_type": "int8",
|
|
"language": language,
|
|
}),
|
|
);
|
|
|
|
let response = manager.send_and_receive(&msg)?;
|
|
|
|
if response.msg_type == "error" {
|
|
return Err(format!(
|
|
"Transcription error: {}",
|
|
response
|
|
.payload
|
|
.get("message")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("unknown")
|
|
));
|
|
}
|
|
|
|
Ok(response.payload)
|
|
}
|
|
|
|
/// Download and validate the diarization model via the Python sidecar.
|
|
#[tauri::command]
|
|
pub fn download_diarize_model(hf_token: String) -> Result<Value, String> {
|
|
let manager = sidecar();
|
|
manager.ensure_running()?;
|
|
|
|
let request_id = uuid::Uuid::new_v4().to_string();
|
|
let msg = IPCMessage::new(
|
|
&request_id,
|
|
"diarize.download",
|
|
json!({
|
|
"hf_token": hf_token,
|
|
}),
|
|
);
|
|
|
|
let response = manager.send_and_receive(&msg)?;
|
|
|
|
if response.msg_type == "error" {
|
|
return Ok(json!({
|
|
"ok": false,
|
|
"error": response.payload.get("message").and_then(|v| v.as_str()).unwrap_or("unknown"),
|
|
}));
|
|
}
|
|
|
|
Ok(json!({ "ok": true }))
|
|
}
|
|
|
|
/// Run the full transcription + diarization pipeline via the Python sidecar.
|
|
#[tauri::command]
|
|
pub async fn run_pipeline(
|
|
app: AppHandle,
|
|
file_path: String,
|
|
model: Option<String>,
|
|
device: Option<String>,
|
|
language: Option<String>,
|
|
num_speakers: Option<u32>,
|
|
min_speakers: Option<u32>,
|
|
max_speakers: Option<u32>,
|
|
skip_diarization: Option<bool>,
|
|
hf_token: Option<String>,
|
|
) -> Result<Value, String> {
|
|
let manager = sidecar();
|
|
manager.ensure_running()?;
|
|
|
|
let request_id = uuid::Uuid::new_v4().to_string();
|
|
let msg = IPCMessage::new(
|
|
&request_id,
|
|
"pipeline.start",
|
|
json!({
|
|
"file": file_path,
|
|
"model": model.unwrap_or_else(|| "base".to_string()),
|
|
"device": device.unwrap_or_else(|| "cpu".to_string()),
|
|
"compute_type": "int8",
|
|
"language": language,
|
|
"num_speakers": num_speakers,
|
|
"min_speakers": min_speakers,
|
|
"max_speakers": max_speakers,
|
|
"skip_diarization": skip_diarization.unwrap_or(false),
|
|
"hf_token": hf_token,
|
|
}),
|
|
);
|
|
|
|
// Run the blocking sidecar I/O on a separate thread so the async runtime
|
|
// can deliver emitted events to the webview while processing is ongoing.
|
|
let app_handle = app.clone();
|
|
tauri::async_runtime::spawn_blocking(move || {
|
|
let response = manager.send_and_receive_with_progress(&msg, |msg| {
|
|
let event_name = match msg.msg_type.as_str() {
|
|
"pipeline.segment" => "pipeline-segment",
|
|
"pipeline.speaker_update" => "pipeline-speaker-update",
|
|
_ => "pipeline-progress",
|
|
};
|
|
if let Err(e) = app_handle.emit(event_name, &msg.payload) {
|
|
eprintln!("[sidecar-rs] Failed to emit {event_name}: {e}");
|
|
}
|
|
})?;
|
|
|
|
if response.msg_type == "error" {
|
|
return Err(format!(
|
|
"Pipeline error: {}",
|
|
response
|
|
.payload
|
|
.get("message")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("unknown")
|
|
));
|
|
}
|
|
|
|
Ok(response.payload)
|
|
})
|
|
.await
|
|
.map_err(|e| format!("Pipeline task failed: {e}"))?
|
|
}
|