Skip to content
Merged
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
66 changes: 33 additions & 33 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 @@ -75,6 +75,7 @@ hypr-importer-core = { path = "crates/importer-core", package = "importer-core"
hypr-intercept = { path = "crates/intercept", package = "intercept" }
hypr-jina = { path = "crates/jina", package = "jina" }
hypr-language = { path = "crates/language", package = "language" }
hypr-listener-core = { path = "crates/listener-core", package = "listener-core" }
hypr-llm-cactus = { path = "crates/llm-cactus", package = "llm-cactus" }
hypr-llm-proxy = { path = "crates/llm-proxy", package = "llm-proxy" }
hypr-llm-types = { path = "crates/llm-types", package = "llm-types" }
Expand Down
43 changes: 43 additions & 0 deletions crates/listener-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[package]
name = "listener-core"
version = "0.1.0"
edition = "2024"

[features]
default = []
specta = ["dep:specta"]

[dependencies]
hypr-aec = { workspace = true }
hypr-audio = { workspace = true }
hypr-audio-utils = { workspace = true }
hypr-device-monitor = { workspace = true }
hypr-host = { workspace = true }
hypr-language = { workspace = true }
hypr-supervisor = { workspace = true }
hypr-vad-ext = { workspace = true }

owhisper-client = { workspace = true }
owhisper-interface = { workspace = true }

hound = { workspace = true }
vorbis_rs = { workspace = true }

ractor = { workspace = true, features = ["async-trait"] }

bytes = { workspace = true }
futures-util = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
specta = { workspace = true, optional = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] }
tokio-stream = { workspace = true }
tokio-util = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }

sentry = { workspace = true }

[dev-dependencies]
tracing-subscriber = { workspace = true }
161 changes: 161 additions & 0 deletions crates/listener-core/examples/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use std::sync::Arc;

use listener_core::{
ListenerRuntime, SessionDataEvent, SessionErrorEvent, SessionLifecycleEvent,
SessionProgressEvent,
actors::{RootActor, RootArgs, RootMsg, SessionParams},
};
use ractor::Actor;

struct CliRuntime {
sessions_dir: std::path::PathBuf,
}

impl ListenerRuntime for CliRuntime {
fn sessions_dir(&self) -> Result<std::path::PathBuf, String> {
Ok(self.sessions_dir.clone())
}

fn emit_lifecycle(&self, event: SessionLifecycleEvent) {
match &event {
SessionLifecycleEvent::Active { session_id, error } => {
if let Some(err) = error {
eprintln!("[lifecycle] active (degraded) session={session_id} error={err:?}");
} else {
eprintln!("[lifecycle] active session={session_id}");
}
}
SessionLifecycleEvent::Inactive { session_id, error } => {
eprintln!("[lifecycle] inactive session={session_id} error={error:?}");
}
SessionLifecycleEvent::Finalizing { session_id } => {
eprintln!("[lifecycle] finalizing session={session_id}");
}
}
}

fn emit_progress(&self, event: SessionProgressEvent) {
match &event {
SessionProgressEvent::AudioInitializing { .. } => {
eprintln!("[progress] audio initializing...");
}
SessionProgressEvent::AudioReady { device, .. } => {
eprintln!("[progress] audio ready device={device:?}");
}
SessionProgressEvent::Connecting { .. } => {
eprintln!("[progress] connecting to STT...");
}
SessionProgressEvent::Connected { adapter, .. } => {
eprintln!("[progress] connected via {adapter}");
}
}
}

fn emit_error(&self, event: SessionErrorEvent) {
match &event {
SessionErrorEvent::AudioError { error, device, .. } => {
eprintln!("[error] audio: {error} device={device:?}");
}
SessionErrorEvent::ConnectionError { error, .. } => {
eprintln!("[error] connection: {error}");
}
}
}

fn emit_data(&self, event: SessionDataEvent) {
match &event {
SessionDataEvent::AudioAmplitude { mic, speaker, .. } => {
let mic_bar = "█".repeat((*mic as usize) / 50);
let spk_bar = "█".repeat((*speaker as usize) / 50);
eprint!("\r[audio] mic {mic_bar:<20} | spk {spk_bar:<20}");
}
SessionDataEvent::StreamResponse { response, .. } => {
println!("{}", serde_json::to_string(&response).unwrap_or_default());
}
SessionDataEvent::MicMuted { value, .. } => {
eprintln!("[data] mic muted={value}");
}
}
}
}

#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.init();

let base_url = std::env::var("LISTENER_BASE_URL").unwrap_or_else(|_| {
eprintln!("Usage: LISTENER_BASE_URL=... LISTENER_API_KEY=... cargo run --example cli");
eprintln!();
eprintln!(" LISTENER_BASE_URL STT provider URL (required)");
eprintln!(" LISTENER_API_KEY API key (default: empty)");
eprintln!(" LISTENER_MODEL Model name (default: empty)");
eprintln!(" LISTENER_LANGUAGE Language code (default: en)");
eprintln!(" LISTENER_RECORD Enable WAV recording (default: false)");
std::process::exit(1);
});

let api_key = std::env::var("LISTENER_API_KEY").unwrap_or_default();
let model = std::env::var("LISTENER_MODEL").unwrap_or_default();
let language = std::env::var("LISTENER_LANGUAGE").unwrap_or_else(|_| "en".into());
let record_enabled = std::env::var("LISTENER_RECORD")
.map(|v| v == "1" || v == "true")
.unwrap_or(false);

let languages = vec![
language
.parse::<hypr_language::Language>()
.expect("invalid language code"),
];

let session_id = uuid::Uuid::new_v4().to_string();
let sessions_dir = std::env::temp_dir().join("listener-cli").join("sessions");

let runtime = Arc::new(CliRuntime { sessions_dir });

let (root_ref, _handle) = Actor::spawn(
Some(RootActor::name()),
RootActor,
RootArgs {
runtime: runtime.clone(),
},
)
.await
.expect("failed to spawn root actor");

eprintln!("Starting session {session_id}...");
eprintln!("Press Ctrl+C to stop.");
eprintln!();

let params = SessionParams {
session_id: session_id.clone(),
languages,
onboarding: false,
record_enabled,
model,
base_url,
api_key,
keywords: vec![],
};

let started = ractor::call!(root_ref, RootMsg::StartSession, params)
.expect("failed to send start message");

if !started {
eprintln!("Failed to start session");
std::process::exit(1);
}

tokio::signal::ctrl_c()
.await
.expect("failed to listen for ctrl+c");

eprintln!();
eprintln!("Stopping session...");

let _ = ractor::call!(root_ref, RootMsg::StopSession);

tokio::time::sleep(std::time::Duration::from_secs(2)).await;
eprintln!("Done.");
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use std::time::{Duration, UNIX_EPOCH};

use bytes::Bytes;
use ractor::{ActorProcessingErr, ActorRef};
use tauri_specta::Event;

use owhisper_client::{
AdapterKind, ArgmaxAdapter, AssemblyAIAdapter, CactusAdapter, DashScopeAdapter,
Expand Down Expand Up @@ -181,20 +180,18 @@ async fn spawn_rx_task_single_with_adapter<A: RealtimeSttAdapter>(
timeout_secs = LISTEN_CONNECT_TIMEOUT.as_secs_f32(),
"listen_ws_connect_timeout(single)"
);
let _ = (SessionErrorEvent::ConnectionError {
args.runtime.emit_error(SessionErrorEvent::ConnectionError {
session_id: args.session_id.clone(),
error: "listen_ws_connect_timeout".to_string(),
})
.emit(&args.app);
});
return Err(actor_error("listen_ws_connect_timeout"));
}
Ok(Err(e)) => {
tracing::error!(session_id = %args.session_id, error = ?e, "listen_ws_connect_failed(single)");
let _ = (SessionErrorEvent::ConnectionError {
args.runtime.emit_error(SessionErrorEvent::ConnectionError {
session_id: args.session_id.clone(),
error: format!("listen_ws_connect_failed: {:?}", e),
})
.emit(&args.app);
});
return Err(actor_error(format!("listen_ws_connect_failed: {:?}", e)));
}
Ok(Ok(res)) => res,
Expand Down Expand Up @@ -253,20 +250,18 @@ async fn spawn_rx_task_dual_with_adapter<A: RealtimeSttAdapter>(
timeout_secs = LISTEN_CONNECT_TIMEOUT.as_secs_f32(),
"listen_ws_connect_timeout(dual)"
);
let _ = (SessionErrorEvent::ConnectionError {
args.runtime.emit_error(SessionErrorEvent::ConnectionError {
session_id: args.session_id.clone(),
error: "listen_ws_connect_timeout".to_string(),
})
.emit(&args.app);
});
return Err(actor_error("listen_ws_connect_timeout"));
}
Ok(Err(e)) => {
tracing::error!(session_id = %args.session_id, error = ?e, "listen_ws_connect_failed(dual)");
let _ = (SessionErrorEvent::ConnectionError {
args.runtime.emit_error(SessionErrorEvent::ConnectionError {
session_id: args.session_id.clone(),
error: format!("listen_ws_connect_failed: {:?}", e),
})
.emit(&args.app);
});
return Err(actor_error(format!("listen_ws_connect_failed: {:?}", e)));
}
Ok(Ok(res)) => res,
Expand Down
Loading