Skip to content
Draft
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
16 changes: 16 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ hypr-mac = { path = "crates/mac", package = "mac" }
hypr-mcp = { path = "crates/mcp", package = "mcp" }
hypr-mobile-bridge = { path = "crates/mobile-bridge", package = "mobile-bridge" }
hypr-model-downloader = { path = "crates/model-downloader", package = "model-downloader" }
hypr-model-manager = { path = "crates/model-manager", package = "model-manager" }
hypr-mp3 = { path = "crates/mp3", package = "mp3" }
hypr-nango = { path = "crates/nango", package = "nango" }
hypr-notification = { path = "crates/notification", package = "notification" }
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/stt/contexts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe("ListenerProvider detect events", () => {
listenMock.mockResolvedValue(() => {});
});

test("does not stop listening when MicStopped arrives", async () => {
test("stops listening when MicStopped arrives", async () => {
const store = createListenerStore();
const stopSpy = vi.fn();

Expand All @@ -65,7 +65,7 @@ describe("ListenerProvider detect events", () => {
},
});

expect(stopSpy).not.toHaveBeenCalled();
expect(stopSpy).toHaveBeenCalledTimes(1);
});

test("stops listening when sleep starts", async () => {
Expand Down
5 changes: 5 additions & 0 deletions crates/cactus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ version = "0.1.0"
edition = "2024"
license = "MIT"

[features]
default = []
model-manager = ["dep:hypr-model-manager"]

[dependencies]
cactus-sys = { git = "https://github.com/cactus-compute/cactus", package = "cactus-sys", rev = "a5acad3" }

hypr-language = { workspace = true }
hypr-llm-types = { workspace = true }
hypr-model-manager = { workspace = true, optional = true }

futures-util = { workspace = true }
tokio = { workspace = true, features = ["rt", "sync"] }
Expand Down
9 changes: 9 additions & 0 deletions crates/cactus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ pub use stt::{
pub use vad::{VadOptions, VadResult, VadSegment};

pub use hypr_llm_types::{Response, StreamingParser};

#[cfg(feature = "model-manager")]
impl hypr_model_manager::ModelLoader for Model {
type Error = Error;

fn load(path: &std::path::Path) -> Result<Self, Self::Error> {
Model::new(path)
}
}
74 changes: 48 additions & 26 deletions crates/listener-core/src/actors/listener/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,79 +30,80 @@ pub(super) async fn spawn_rx_task(
let adapter_kind =
AdapterKind::from_url_and_languages(&args.base_url, &args.languages, Some(&args.model));
let is_dual = matches!(args.mode, crate::actors::ChannelMode::MicAndSpeaker);
let policy = connect_policy_for(adapter_kind);

let result = match (adapter_kind, is_dual) {
(AdapterKind::Argmax, false) => {
spawn_rx_task_single_with_adapter::<ArgmaxAdapter>(args, myself).await
spawn_rx_task_single_with_adapter::<ArgmaxAdapter>(args, myself, policy).await
}
(AdapterKind::Argmax, true) => {
spawn_rx_task_dual_with_adapter::<ArgmaxAdapter>(args, myself).await
spawn_rx_task_dual_with_adapter::<ArgmaxAdapter>(args, myself, policy).await
}
(AdapterKind::Soniox, false) => {
spawn_rx_task_single_with_adapter::<SonioxAdapter>(args, myself).await
spawn_rx_task_single_with_adapter::<SonioxAdapter>(args, myself, policy).await
}
(AdapterKind::Soniox, true) => {
spawn_rx_task_dual_with_adapter::<SonioxAdapter>(args, myself).await
spawn_rx_task_dual_with_adapter::<SonioxAdapter>(args, myself, policy).await
}
(AdapterKind::Fireworks, false) => {
spawn_rx_task_single_with_adapter::<FireworksAdapter>(args, myself).await
spawn_rx_task_single_with_adapter::<FireworksAdapter>(args, myself, policy).await
}
(AdapterKind::Fireworks, true) => {
spawn_rx_task_dual_with_adapter::<FireworksAdapter>(args, myself).await
spawn_rx_task_dual_with_adapter::<FireworksAdapter>(args, myself, policy).await
}
(AdapterKind::Deepgram, false) => {
spawn_rx_task_single_with_adapter::<DeepgramAdapter>(args, myself).await
spawn_rx_task_single_with_adapter::<DeepgramAdapter>(args, myself, policy).await
}
(AdapterKind::Deepgram, true) => {
spawn_rx_task_dual_with_adapter::<DeepgramAdapter>(args, myself).await
spawn_rx_task_dual_with_adapter::<DeepgramAdapter>(args, myself, policy).await
}
(AdapterKind::AssemblyAI, false) => {
spawn_rx_task_single_with_adapter::<AssemblyAIAdapter>(args, myself).await
spawn_rx_task_single_with_adapter::<AssemblyAIAdapter>(args, myself, policy).await
}
(AdapterKind::AssemblyAI, true) => {
spawn_rx_task_dual_with_adapter::<AssemblyAIAdapter>(args, myself).await
spawn_rx_task_dual_with_adapter::<AssemblyAIAdapter>(args, myself, policy).await
}
(AdapterKind::OpenAI, false) => {
spawn_rx_task_single_with_adapter::<OpenAIAdapter>(args, myself).await
spawn_rx_task_single_with_adapter::<OpenAIAdapter>(args, myself, policy).await
}
(AdapterKind::OpenAI, true) => {
spawn_rx_task_dual_with_adapter::<OpenAIAdapter>(args, myself).await
spawn_rx_task_dual_with_adapter::<OpenAIAdapter>(args, myself, policy).await
}
(AdapterKind::Gladia, false) => {
spawn_rx_task_single_with_adapter::<GladiaAdapter>(args, myself).await
spawn_rx_task_single_with_adapter::<GladiaAdapter>(args, myself, policy).await
}
(AdapterKind::Gladia, true) => {
spawn_rx_task_dual_with_adapter::<GladiaAdapter>(args, myself).await
spawn_rx_task_dual_with_adapter::<GladiaAdapter>(args, myself, policy).await
}
(AdapterKind::ElevenLabs, false) => {
spawn_rx_task_single_with_adapter::<ElevenLabsAdapter>(args, myself).await
spawn_rx_task_single_with_adapter::<ElevenLabsAdapter>(args, myself, policy).await
}
(AdapterKind::ElevenLabs, true) => {
spawn_rx_task_dual_with_adapter::<ElevenLabsAdapter>(args, myself).await
spawn_rx_task_dual_with_adapter::<ElevenLabsAdapter>(args, myself, policy).await
}
(AdapterKind::DashScope, false) => {
spawn_rx_task_single_with_adapter::<DashScopeAdapter>(args, myself).await
spawn_rx_task_single_with_adapter::<DashScopeAdapter>(args, myself, policy).await
}
(AdapterKind::DashScope, true) => {
spawn_rx_task_dual_with_adapter::<DashScopeAdapter>(args, myself).await
spawn_rx_task_dual_with_adapter::<DashScopeAdapter>(args, myself, policy).await
}
(AdapterKind::Mistral, false) => {
spawn_rx_task_single_with_adapter::<MistralAdapter>(args, myself).await
spawn_rx_task_single_with_adapter::<MistralAdapter>(args, myself, policy).await
}
(AdapterKind::Mistral, true) => {
spawn_rx_task_dual_with_adapter::<MistralAdapter>(args, myself).await
spawn_rx_task_dual_with_adapter::<MistralAdapter>(args, myself, policy).await
}
(AdapterKind::Hyprnote, false) => {
spawn_rx_task_single_with_adapter::<HyprnoteAdapter>(args, myself).await
spawn_rx_task_single_with_adapter::<HyprnoteAdapter>(args, myself, policy).await
}
(AdapterKind::Hyprnote, true) => {
spawn_rx_task_dual_with_adapter::<HyprnoteAdapter>(args, myself).await
spawn_rx_task_dual_with_adapter::<HyprnoteAdapter>(args, myself, policy).await
}
(AdapterKind::Cactus, false) => {
spawn_rx_task_single_with_adapter::<CactusAdapter>(args, myself).await
spawn_rx_task_single_with_adapter::<CactusAdapter>(args, myself, policy).await
}
(AdapterKind::Cactus, true) => {
spawn_rx_task_dual_with_adapter::<CactusAdapter>(args, myself).await
spawn_rx_task_dual_with_adapter::<CactusAdapter>(args, myself, policy).await
}
}?;

Expand Down Expand Up @@ -148,9 +149,29 @@ fn desktop_connect_policy() -> hypr_ws_client::client::WebSocketConnectPolicy {
}
}

fn local_model_connect_policy() -> hypr_ws_client::client::WebSocketConnectPolicy {
hypr_ws_client::client::WebSocketConnectPolicy {
connect_timeout: Duration::from_secs(10),
max_attempts: 15,
retry_delay: Duration::from_secs(5),
}
}

fn connect_policy_for(
kind: owhisper_client::AdapterKind,
) -> hypr_ws_client::client::WebSocketConnectPolicy {
match kind {
owhisper_client::AdapterKind::Cactus | owhisper_client::AdapterKind::Argmax => {
local_model_connect_policy()
}
_ => desktop_connect_policy(),
}
}

async fn spawn_rx_task_single_with_adapter<A: RealtimeSttAdapter>(
args: ListenerArgs,
myself: ActorRef<ListenerMsg>,
policy: hypr_ws_client::client::WebSocketConnectPolicy,
) -> Result<
(
ChannelSender,
Expand All @@ -169,7 +190,7 @@ async fn spawn_rx_task_single_with_adapter<A: RealtimeSttAdapter>(
.api_base(args.base_url.clone())
.api_key(args.api_key.clone())
.params(build_listen_params(&args))
.connect_policy(desktop_connect_policy())
.connect_policy(policy)
.extra_header(DEVICE_FINGERPRINT_HEADER, hypr_host::fingerprint())
.build_single()
.await;
Expand Down Expand Up @@ -211,6 +232,7 @@ async fn spawn_rx_task_single_with_adapter<A: RealtimeSttAdapter>(
async fn spawn_rx_task_dual_with_adapter<A: RealtimeSttAdapter>(
args: ListenerArgs,
myself: ActorRef<ListenerMsg>,
policy: hypr_ws_client::client::WebSocketConnectPolicy,
) -> Result<
(
ChannelSender,
Expand All @@ -229,7 +251,7 @@ async fn spawn_rx_task_dual_with_adapter<A: RealtimeSttAdapter>(
.api_base(args.base_url.clone())
.api_key(args.api_key.clone())
.params(build_listen_params(&args))
.connect_policy(desktop_connect_policy())
.connect_policy(policy)
.extra_header(DEVICE_FINGERPRINT_HEADER, hypr_host::fingerprint())
.build_dual()
.await;
Expand Down
2 changes: 2 additions & 0 deletions crates/listener-core/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub enum SessionProgressEvent {
Connecting { session_id: String },
#[serde(rename = "connected")]
Connected { session_id: String, adapter: String },
#[serde(rename = "model_loading")]
ModelLoading { session_id: String },
}

#[derive(serde::Serialize, serde::Deserialize, Clone)]
Expand Down
2 changes: 1 addition & 1 deletion crates/listener2-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ hypr-language = { workspace = true }
bytes = { workspace = true }
hound = { workspace = true }

owhisper-client = { workspace = true, features = ["argmax"] }
owhisper-client = { workspace = true, features = ["local"] }
owhisper-interface = { workspace = true }

serde = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions crates/listener2-core/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ pub enum BatchEvent {
code: BatchErrorCode,
error: String,
},
#[serde(rename = "modelLoading")]
ModelLoading { session_id: String },
}

#[derive(serde::Serialize, Clone)]
Expand Down
3 changes: 2 additions & 1 deletion crates/llm-cactus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ version = "0.1.0"
edition = "2024"

[dependencies]
hypr-cactus = { workspace = true }
hypr-cactus = { workspace = true, features = ["model-manager"] }
hypr-llm-types = { workspace = true }
hypr-model-manager = { workspace = true }

serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
Expand Down
10 changes: 2 additions & 8 deletions crates/llm-cactus/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@
pub enum Error {
#[error(transparent)]
Cactus(#[from] hypr_cactus::Error),
#[error("model not registered: {0}")]
ModelNotRegistered(String),
#[error("model file not found: {0}")]
ModelFileNotFound(String),
#[error("no default model configured")]
NoDefaultModel,
#[error("worker task panicked")]
WorkerPanicked,
#[error(transparent)]
ModelManager(#[from] hypr_model_manager::Error<hypr_cactus::Error>),
}
Loading
Loading