Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ mesh-llm-events = { git = "https://github.com/Mesh-LLM/mesh-llm.git", rev = "f45
base64 = "0.22"
sha2 = "0.11"
tar = "0.4"
bzip2 = "0.6"
chrono = { version = "0.4", features = ["serde"] }
tauri-plugin-global-shortcut = "2"
tauri-plugin-notification = "2.3.3"
Expand Down
6 changes: 4 additions & 2 deletions desktop/src-tauri/src/huddle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub mod pocket;
pub mod preprocessing;
pub mod reconnect;
pub mod relay_api;
pub mod settings;
pub mod state;
pub mod stt;
pub mod transcription;
Expand Down Expand Up @@ -61,7 +62,8 @@ pub(super) fn drain_until_shutdown<T>(

// ── Re-exports ────────────────────────────────────────────────────────────────

pub use state::{HuddleJoinInfo, HuddlePhase, HuddleState, VoiceInputMode};
pub use settings::{get_transcription_language, set_transcription_language};
pub use state::{HuddleJoinInfo, HuddlePhase, HuddleState, TranscriptionLanguage, VoiceInputMode};
pub use transcription::{set_huddle_transcription_enabled, start_stt_pipeline};

// ── Imports ───────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -792,7 +794,7 @@ pub async fn check_pipeline_hotstart(state: State<'_, AppState>) -> Result<(), S
Ok(())
}

/// Trigger a background download of voice models (Parakeet STT + Pocket TTS).
/// Trigger a background download of voice models (multilingual Whisper STT + Pocket TTS).
///
/// Returns immediately — downloads run in tokio background tasks.
/// Poll `get_model_status` to track progress.
Expand Down
365 changes: 149 additions & 216 deletions desktop/src-tauri/src/huddle/models.rs

Large diffs are not rendered by default.

19 changes: 17 additions & 2 deletions desktop/src-tauri/src/huddle/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,15 @@ pub(crate) async fn maybe_start_stt_pipeline(
// transcription task's next POST sees a stale generation and exits.
// Take the old pipeline OUT of the lock before dropping — Drop joins
// the worker thread (~200ms) and must not block under the mutex.
let (tts_active, tts_cancel, agent_pubkeys_arc, session_gen, ptt_active_for_stt, old_stt) = {
let (
tts_active,
tts_cancel,
agent_pubkeys_arc,
session_gen,
ptt_active_for_stt,
transcription_language,
old_stt,
) = {
let mut hs = state.huddle()?;
// Invalidate any existing transcription task before replacing the pipeline.
if hs.stt_pipeline.is_some() {
Expand All @@ -131,14 +139,21 @@ pub(crate) async fn maybe_start_stt_pipeline(
Arc::clone(&hs.agent_pubkeys),
Arc::clone(&hs.session_generation),
ptt,
hs.transcription_language,
old,
)
};
// Drop the old pipeline OUTSIDE the lock — thread join happens here.
drop(old_stt);

let constructed = tokio::task::spawn_blocking(move || {
stt::SttPipeline::new(model_dir, tts_active, tts_cancel, ptt_active_for_stt)
stt::SttPipeline::new(
model_dir,
transcription_language,
tts_active,
tts_cancel,
ptt_active_for_stt,
)
})
.await;
let (pipeline, text_rx) = match constructed {
Expand Down
56 changes: 56 additions & 0 deletions desktop/src-tauri/src/huddle/settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! User-selectable Huddle speech settings.

use tauri::State;

use crate::app_state::AppState;

use super::{pipeline::maybe_start_stt_pipeline, HuddlePhase, TranscriptionLanguage};

/// Set the language used by the local multilingual Whisper recognizer.
///
/// The recognizer captures its language at construction time, so changing the
/// selection during an active transcribed huddle restarts only the STT pipeline.
#[tauri::command]
pub async fn set_transcription_language(
language: TranscriptionLanguage,
state: State<'_, AppState>,
) -> Result<(), String> {
let needs_restart = {
let mut hs = state.huddle()?;
let old_language = hs.transcription_language;
hs.transcription_language = language;
old_language != language
&& matches!(hs.phase, HuddlePhase::Connected | HuddlePhase::Active)
&& hs.stt_pipeline.is_some()
&& hs.transcription_enabled
};

let restart_result = if needs_restart {
let eph_id = {
let hs = state.huddle()?;
hs.ephemeral_channel_id.clone()
};
if let Some(eph_id) = eph_id {
maybe_start_stt_pipeline(&state, &eph_id)
.await
.map(|_| ())
.map_err(|e| format!("restart transcription for language change: {e}"))
} else {
Ok(())
}
} else {
Ok(())
};

state.emit_huddle_state_changed();
restart_result
}

/// Return the language used by the local multilingual Whisper recognizer.
#[tauri::command]
pub fn get_transcription_language(
state: State<'_, AppState>,
) -> Result<TranscriptionLanguage, String> {
let hs = state.huddle()?;
Ok(hs.transcription_language)
}
27 changes: 26 additions & 1 deletion desktop/src-tauri/src/huddle/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ pub enum VoiceInputMode {
VoiceActivity,
}

/// Language passed to the local multilingual Whisper recognizer.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TranscriptionLanguage {
German,
#[default]
English,
}

impl TranscriptionLanguage {
pub(crate) const fn whisper_code(self) -> &'static str {
match self {
Self::German => "de",
Self::English => "en",
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum HuddlePhase {
Expand Down Expand Up @@ -80,6 +98,8 @@ pub struct HuddleState {
pub tts_enabled: bool,
/// Whether STT transcript posting is enabled for this huddle.
pub transcription_enabled: bool,
/// Language used by the local multilingual Whisper recognizer.
pub transcription_language: TranscriptionLanguage,
/// Shared flag: true while TTS is playing audio.
/// Shared with the STT pipeline for barge-in / echo gating.
#[serde(skip)]
Expand Down Expand Up @@ -157,6 +177,7 @@ impl Clone for HuddleState {
is_creator: self.is_creator,
tts_enabled: self.tts_enabled,
transcription_enabled: self.transcription_enabled,
transcription_language: self.transcription_language,
tts_active: Arc::clone(&self.tts_active),
tts_cancel: Arc::clone(&self.tts_cancel),
tts_starting: Arc::clone(&self.tts_starting),
Expand Down Expand Up @@ -184,6 +205,7 @@ impl Default for HuddleState {
is_creator: false,
tts_enabled: true,
transcription_enabled: false,
transcription_language: TranscriptionLanguage::default(),
tts_active: Arc::new(AtomicBool::new(false)),
tts_cancel: Arc::new(AtomicBool::new(false)),
tts_starting: Arc::new(AtomicBool::new(false)),
Expand All @@ -197,13 +219,16 @@ impl Default for HuddleState {
}

impl HuddleState {
/// Reset to default state while preserving the session generation counter.
/// Reset to default state while preserving user voice preferences and the
/// session generation counter.
/// Used by start_huddle rollback, join_huddle rollback, and teardown_huddle
/// to invalidate in-flight transcription tasks without losing the generation.
pub(crate) fn reset_preserving_generation(&mut self) {
let gen = Arc::clone(&self.session_generation);
let transcription_language = self.transcription_language;
*self = Self::default();
self.session_generation = gen;
self.transcription_language = transcription_language;
}
}

Expand Down
Loading