Fix progress feedback, diarization fallback, and dropdown readability

- Stream pipeline progress to frontend via Tauri events so the progress
  overlay updates in real time during transcription/diarization
- Gracefully fall back to transcription-only when diarization fails
  (e.g. pyannote not installed) instead of erroring the whole pipeline
- Add color-scheme: dark to fix native select/option elements rendering
  with unreadable white backgrounds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 17:14:25 -08:00
parent d00281f0c7
commit 669d88f143
5 changed files with 81 additions and 21 deletions

View File

@@ -110,11 +110,13 @@ class PipelineService:
)
return result
# Step 2: Diarize
# Step 2: Diarize (with graceful fallback)
write_message(
progress_message(request_id, 50, "pipeline", "Starting speaker diarization...")
)
diarization = None
try:
diarization = self._diarize_service.diarize(
request_id=request_id,
file_path=file_path,
@@ -122,15 +124,43 @@ class PipelineService:
min_speakers=min_speakers,
max_speakers=max_speakers,
)
except Exception as e:
print(
f"[sidecar] Diarization failed, falling back to transcription-only: {e}",
file=sys.stderr,
flush=True,
)
write_message(
progress_message(
request_id, 80, "pipeline",
"Diarization unavailable, using transcription only..."
)
)
# Step 3: Merge
# Step 3: Merge (or skip if diarization failed)
if diarization is not None:
write_message(
progress_message(request_id, 90, "pipeline", "Merging transcript with speakers...")
)
result = self._merge_results(transcription, diarization.speaker_segments)
result.speakers = diarization.speakers
result.num_speakers = diarization.num_speakers
else:
result = PipelineResult(
language=transcription.language,
language_probability=transcription.language_probability,
duration_ms=transcription.duration_ms,
)
for seg in transcription.segments:
result.segments.append(
PipelineSegment(
text=seg.text,
start_ms=seg.start_ms,
end_ms=seg.end_ms,
speaker=None,
words=seg.words,
)
)
elapsed = time.time() - start_time
print(

View File

@@ -1,4 +1,5 @@
use serde_json::{json, Value};
use tauri::{AppHandle, Emitter};
use crate::sidecar::messages::IPCMessage;
use crate::sidecar::sidecar;
@@ -42,6 +43,7 @@ pub fn transcribe_file(
/// Run the full transcription + diarization pipeline via the Python sidecar.
#[tauri::command]
pub fn run_pipeline(
app: AppHandle,
file_path: String,
model: Option<String>,
device: Option<String>,
@@ -71,7 +73,9 @@ pub fn run_pipeline(
}),
);
let response = manager.send_and_receive(&msg)?;
let response = manager.send_and_receive_with_progress(&msg, |progress| {
let _ = app.emit("pipeline-progress", &progress.payload);
})?;
if response.msg_type == "error" {
return Err(format!(

View File

@@ -115,8 +115,17 @@ impl SidecarManager {
}
/// Send a message to the sidecar and read the response.
/// This is a blocking call.
/// This is a blocking call. Progress messages are skipped.
pub fn send_and_receive(&self, msg: &IPCMessage) -> Result<IPCMessage, String> {
self.send_and_receive_with_progress(msg, |_| {})
}
/// Send a message and read the response, calling on_progress for each progress message.
pub fn send_and_receive_with_progress(
&self,
msg: &IPCMessage,
on_progress: impl Fn(&IPCMessage),
) -> Result<IPCMessage, String> {
// Write to stdin
{
let mut stdin_guard = self.stdin.lock().map_err(|e| e.to_string())?;
@@ -154,10 +163,11 @@ impl SidecarManager {
let response: IPCMessage = serde_json::from_str(trimmed)
.map_err(|e| format!("Parse error: {e}"))?;
// Skip progress messages, return the final result/error
if response.msg_type != "progress" {
return Ok(response);
if response.msg_type == "progress" {
on_progress(&response);
continue;
}
return Ok(response);
}
} else {
Err("Sidecar stdout not available".to_string())

View File

@@ -10,6 +10,7 @@
padding: 0;
background: #0a0a23;
color: #e0e0e0;
color-scheme: dark;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, sans-serif;
overflow: hidden;

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { invoke, convertFileSrc } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { open, save } from '@tauri-apps/plugin-dialog';
import WaveformPlayer from '$lib/components/WaveformPlayer.svelte';
import TranscriptEditor from '$lib/components/TranscriptEditor.svelte';
@@ -89,6 +90,19 @@
isTranscribing = true;
transcriptionProgress = 0;
transcriptionStage = 'Starting...';
transcriptionMessage = 'Initializing pipeline...';
// Listen for progress events from the sidecar
const unlisten = await listen<{
percent: number;
stage: string;
message: string;
}>('pipeline-progress', (event) => {
const { percent, stage, message } = event.payload;
if (typeof percent === 'number') transcriptionProgress = percent;
if (typeof stage === 'string') transcriptionStage = stage;
if (typeof message === 'string') transcriptionMessage = message;
});
try {
const result = await invoke<{
@@ -159,6 +173,7 @@
console.error('Pipeline failed:', err);
alert(`Pipeline failed: ${err}`);
} finally {
unlisten();
isTranscribing = false;
}
}