From 0ba0e953a87d2b9855afcc8609d62dde38fc8f48 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:40:16 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`linux-d?= =?UTF-8?q?evelopment`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @PythonTilk. * https://github.com/PythonTilk/hyprnote/pull/1#issuecomment-3511664778 The following files were modified: * `crates/audio/src/bin/test_mic.rs` * `crates/audio/src/bin/test_speaker.rs` * `crates/audio/src/lib.rs` * `crates/audio/src/mic.rs` * `crates/audio/src/speaker/linux.rs` * `crates/audio/src/speaker/mod.rs` * `crates/detect/src/app/linux.rs` * `crates/detect/src/browser/linux.rs` * `crates/detect/src/mic/linux.rs` * `crates/file/examples/checksum.rs` * `crates/transcribe-whisper-local/examples/show_data_dir.rs` * `crates/whisper-local/examples/list_backends.rs` * `crates/whisper-local/examples/test_model.rs` * `crates/whisper-local/src/model.rs` * `owhisper/owhisper-server/src/commands/run.rs` * `plugins/listener/src/error.rs` * `plugins/listener/src/ext.rs` * `plugins/listener/src/fsm.rs` --- crates/audio/src/bin/test_mic.rs | 12 ++ crates/audio/src/bin/test_speaker.rs | 14 ++ crates/audio/src/lib.rs | 57 +++++++- crates/audio/src/mic.rs | 38 +++++- crates/audio/src/speaker/linux.rs | 87 +++++++++++- crates/audio/src/speaker/mod.rs | 119 ++++++++++++++++- crates/detect/src/app/linux.rs | 42 ++++++ crates/detect/src/browser/linux.rs | 32 +++++ crates/detect/src/mic/linux.rs | 51 +++++++ crates/file/examples/checksum.rs | 14 ++ .../examples/show_data_dir.rs | 11 ++ .../whisper-local/examples/list_backends.rs | 16 +++ crates/whisper-local/examples/test_model.rs | 10 ++ crates/whisper-local/src/model.rs | 21 ++- owhisper/owhisper-server/src/commands/run.rs | 19 ++- plugins/listener/src/error.rs | 15 ++- plugins/listener/src/ext.rs | 126 +++++++++++++++++- plugins/listener/src/fsm.rs | 86 +++++++++++- 18 files changed, 756 insertions(+), 14 deletions(-) diff --git a/crates/audio/src/bin/test_mic.rs b/crates/audio/src/bin/test_mic.rs index b2a7f8cc3..fff8158ab 100644 --- a/crates/audio/src/bin/test_mic.rs +++ b/crates/audio/src/bin/test_mic.rs @@ -1,5 +1,17 @@ use audio::AudioInput; +/// Tests microphone access by listing available devices and attempting to create audio input. +/// +/// Prints available microphone devices, the default device name, and then tries to create an `AudioInput` +/// using the default device, the first enumerated device (if any), and a set of known device names, +/// reporting success or failure to stdout. +/// +/// # Examples +/// +/// ```no_run +/// // Invoke the binary entry point which performs device enumeration and connection attempts. +/// main(); +/// ``` fn main() { println!("Testing microphone access..."); diff --git a/crates/audio/src/bin/test_speaker.rs b/crates/audio/src/bin/test_speaker.rs index 1d2f14da1..2135a4fbc 100644 --- a/crates/audio/src/bin/test_speaker.rs +++ b/crates/audio/src/bin/test_speaker.rs @@ -1,5 +1,19 @@ use audio::AudioInput; +/// Runs a simple binary test that creates an `AudioInput` from the default speaker, obtains its stream, and prints which `AudioStream` variant was returned. +/// +/// This program exercises creation of a speaker `AudioInput`, requests its stream, and reports whether the stream is a `RealtimeSpeaker` variant. It does not poll or consume audio samples. +/// +/// # Examples +/// +/// ``` +/// // Call the test binary's main to perform the creation and type check. +/// // The example demonstrates the intended usage; output is printed to stdout. +/// fn run() { +/// crate::main(); +/// } +/// run(); +/// ``` fn main() { println!("Testing SpeakerInput creation..."); diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs index c70221cde..4576426ac 100644 --- a/crates/audio/src/lib.rs +++ b/crates/audio/src/lib.rs @@ -69,6 +69,18 @@ pub struct AudioInput { } impl AudioInput { + /// Get the name of the system's default input (microphone) device. + /// + /// # Returns + /// + /// A `String` with the device name, `"Unknown Microphone"` if the device exists but has no name, or `"No Microphone Available"` if there is no default input device. + /// + /// # Examples + /// + /// ``` + /// let name = get_default_mic_device_name(); + /// assert!(!name.is_empty()); + /// ``` pub fn get_default_mic_device_name() -> String { let host = cpal::default_host(); if let Some(device) = host.default_input_device() { @@ -78,6 +90,19 @@ impl AudioInput { } } + /// Returns a list of available input (microphone) device names. + /// + /// The returned list contains the names of enumerated input devices. It filters out the + /// "hypr-audio-tap" device and will append the virtual "echo-cancel-source" device if + /// `pactl list sources short` reports it and it is not already present. + /// + /// # Examples + /// + /// ``` + /// let devices = crate::audio::list_mic_devices(); + /// // devices is a Vec of device names (may be empty) + /// assert!(devices.is_empty() || devices.iter().all(|s| !s.is_empty())); + /// ``` pub fn list_mic_devices() -> Vec { let host = cpal::default_host(); @@ -133,6 +158,20 @@ impl AudioInput { result } + /// Creates an AudioInput configured to stream from a microphone. + /// + /// If `device_name` is `Some(name)`, attempts to open the input device with that name; if `None`, uses the default input device. On success returns an `AudioInput` with `source` set to `RealtimeMic` and `mic` initialized. + /// + /// # Errors + /// + /// Returns a `crate::Error` if microphone initialization fails. + /// + /// # Examples + /// + /// ``` + /// let ai = AudioInput::from_mic(None).expect("failed to open default microphone"); + /// assert!(matches!(ai.source, AudioSource::RealtimeMic)); + /// ``` pub fn from_mic(device_name: Option) -> Result { tracing::info!("Creating AudioInput from microphone with device name: {:?}", device_name); let mic = MicInput::new(device_name)?; @@ -146,6 +185,22 @@ impl AudioInput { }) } + /// Creates an AudioInput configured to capture audio from the system speaker. + /// + /// The returned `AudioInput` uses `AudioSource::RealtimeSpeaker`. The `speaker` field will + /// contain `Some(SpeakerInput)` if speaker capture initialization succeeds, or `None` if it fails; + /// `mic` and `data` are always `None`. + /// + /// # Examples + /// + /// ``` + /// let input = AudioInput::from_speaker(); + /// // `input` is configured for realtime speaker capture; speaker initialization may have failed. + /// match input.source { + /// AudioSource::RealtimeSpeaker => {}, + /// _ => panic!("expected RealtimeSpeaker"), + /// } + /// ``` pub fn from_speaker() -> Self { tracing::debug!("Creating AudioInput from speaker"); let speaker = match SpeakerInput::new() { @@ -248,4 +303,4 @@ impl kalosm_sound::AsyncSource for AudioStream { AudioStream::Recorded { .. } => 16000, } } -} +} \ No newline at end of file diff --git a/crates/audio/src/mic.rs b/crates/audio/src/mic.rs index f1f7b6228..586b6738a 100644 --- a/crates/audio/src/mic.rs +++ b/crates/audio/src/mic.rs @@ -23,6 +23,17 @@ impl MicInput { .unwrap_or("Unknown Microphone".to_string()) } + /// List available input audio device names. + /// + /// A Vec containing the names of available input devices. If a device's + /// name cannot be retrieved, the entry will be "Unknown Microphone". + /// + /// # Examples + /// + /// ``` + /// let names = list_devices(); + /// assert!(names.iter().all(|n| !n.is_empty())); + /// ``` pub fn list_devices() -> Vec { cpal::default_host() .input_devices() @@ -31,7 +42,30 @@ impl MicInput { .collect() } - pub fn new(device_name: Option) -> Result { + /// Creates a new MicInput by selecting and configuring an available input device. + /// + /// This tries to select the requested device when `device_name` is Some, otherwise it prefers + /// the system default input device and falls back to the first enumerated input device. If no + /// devices are directly usable the initializer attempts platform-specific fallbacks (for example + /// handling echo-cancel-source and ALSA probes) before returning an error. + /// + /// # Parameters + /// + /// - `device_name`: Optional device name to prefer; when `None` the function will use the default + /// input device if valid, otherwise the first available device. + /// + /// # Returns + /// + /// `Ok(Self)` with the chosen host, device, and supported stream configuration on success, + /// `Err(crate::Error::NoInputDevice)` if no usable input device or configuration can be found. + /// + /// # Examples + /// + /// ``` + /// // Create a MicInput using the default device (or fallbacks). + /// let _ = MicInput::new(None); + /// ``` + pub fn new(device_name: Option) -> Result { let host = cpal::default_host(); tracing::info!("Initializing microphone input..."); @@ -537,4 +571,4 @@ mod tests { assert!(buffer.iter().any(|x| *x != 0.0)); } -} +} \ No newline at end of file diff --git a/crates/audio/src/speaker/linux.rs b/crates/audio/src/speaker/linux.rs index 20d17e9dc..fb40e45ee 100644 --- a/crates/audio/src/speaker/linux.rs +++ b/crates/audio/src/speaker/linux.rs @@ -8,11 +8,31 @@ use std::time::Duration; pub struct SpeakerInput {} impl SpeakerInput { + /// Construct a new Linux SpeakerInput handle. + /// + /// Returns `Ok(Self)` on success, or an `anyhow::Error` if creation fails. + /// + /// # Examples + /// + /// ``` + /// let input = SpeakerInput::new().unwrap(); + /// ``` pub fn new() -> Result { tracing::debug!("Creating Linux SpeakerInput"); Ok(Self {}) } + /// Creates a `SpeakerStream` for receiving speaker input samples. + /// + /// Returns a `SpeakerStream` that yields `f32` audio samples (silence in the current mock). + /// + /// # Examples + /// + /// ``` + /// let input = crate::speaker::linux::SpeakerInput::new().unwrap(); + /// let mut stream = input.stream(); + /// assert_eq!(stream.sample_rate(), 48000); + /// ``` pub fn stream(self) -> SpeakerStream { tracing::debug!("Creating Linux SpeakerStream"); SpeakerStream::new() @@ -25,6 +45,20 @@ pub struct SpeakerStream { } impl SpeakerStream { + /// Creates a new `SpeakerStream` that produces a continuous stream of silence. + /// + /// The returned stream delivers `f32` audio samples (silence as `0.0`) and preserves + /// a background thread for sample production until the stream is dropped. + /// + /// # Examples + /// + /// ``` + /// use futures::stream::StreamExt; + /// + /// let mut stream = SpeakerStream::new(); + /// let sample = futures::executor::block_on(async { stream.next().await }).unwrap(); + /// assert_eq!(sample, 0.0); + /// ``` pub fn new() -> Self { tracing::debug!("Creating Linux SpeakerStream"); // For now, we'll create a mock implementation that generates silence @@ -53,6 +87,20 @@ impl SpeakerStream { } } + /// Audio sample rate for the speaker stream. + /// + /// The method reports the sample rate used for audio frames. + /// + /// # Returns + /// + /// The sample rate in hertz (48000). + /// + /// # Examples + /// + /// ``` + /// let stream = SpeakerStream::new(); + /// assert_eq!(stream.sample_rate(), 48000); + /// ``` pub fn sample_rate(&self) -> u32 { 48000 // Standard sample rate } @@ -61,6 +109,32 @@ impl SpeakerStream { impl Stream for SpeakerStream { type Item = f32; + /// Polls the stream for the next audio sample from the internal channel. + /// + /// Returns `Poll::Ready(Some(sample))` when a sample is available, `Poll::Pending` and + /// schedules the task to be woken when no sample is currently available, and + /// `Poll::Ready(None)` when the producer side of the channel has been disconnected, + /// signalling the end of the stream. + /// + /// # Examples + /// + /// ``` + /// use futures::stream::StreamExt; + /// use std::pin::Pin; + /// + /// // Create the speaker stream and pin it for polling. + /// let stream = crate::speaker::linux::SpeakerStream::new(); + /// let mut pinned = Box::pin(stream); + /// + /// // Poll the stream asynchronously to get the next sample. + /// let sample = futures::executor::block_on(async { + /// pinned.as_mut().next().await + /// }); + /// + /// // The implementation sends silence (0.0) periodically, so we should get a sample + /// // while the background producer thread is running. + /// assert!(sample.is_some()); + /// ``` fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, @@ -78,9 +152,20 @@ impl Stream for SpeakerStream { } impl Drop for SpeakerStream { + /// Logs when the SpeakerStream is dropped and allows its background producer to terminate by closing the channel. + /// + /// Dropping the stream closes its receiving endpoint; the background thread will observe the channel closure and exit. + /// + /// # Examples + /// + /// ``` + /// # use crates::audio::speaker::linux::SpeakerStream; + /// let stream = SpeakerStream::new(); + /// drop(stream); + /// ``` fn drop(&mut self) { // The thread will automatically exit when the sender is dropped // and the receiver gets a Disconnected error tracing::debug!("Dropping SpeakerStream"); } -} +} \ No newline at end of file diff --git a/crates/audio/src/speaker/mod.rs b/crates/audio/src/speaker/mod.rs index 79c47c497..b1c1452ab 100644 --- a/crates/audio/src/speaker/mod.rs +++ b/crates/audio/src/speaker/mod.rs @@ -29,25 +29,78 @@ pub struct SpeakerInput { } impl SpeakerInput { + /// Creates a platform-specific speaker input initialized for the current OS. + /// + /// # Returns + /// + /// `Ok(Self)` containing a `SpeakerInput` if initialization succeeds, `Err` with the underlying error otherwise. + /// + /// # Examples + /// + /// ```no_run + /// let input = SpeakerInput::new().expect("failed to create speaker input"); + /// let _stream = input.stream().expect("failed to open speaker stream"); + /// ``` #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] pub fn new() -> Result { let inner = PlatformSpeakerInput::new()?; Ok(Self { inner }) } - #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + /// Indicates that SpeakerInput is unsupported on the current platform. + /// + /// # Returns + /// + /// An `Err` containing an error stating that `SpeakerInput::new` is not supported on this platform. + /// + /// # Examples + /// + /// ``` + /// // This function is compiled only on unsupported platforms. + /// let err = crate::speaker::SpeakerInput::new().unwrap_err(); + /// let msg = format!("{}", err); + /// assert!(msg.contains("SpeakerInput::new") || msg.contains("not supported")); + /// ``` pub fn new() -> Result { Err(anyhow::anyhow!( "'SpeakerInput::new' is not supported on this platform" )) } - #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] + /// Create a `SpeakerStream` by consuming this `SpeakerInput`. + /// + /// # Returns + /// + /// A `Result` containing the created `SpeakerStream` on success, or an error describing why the stream could not be created. + /// + /// # Examples + /// + /// ``` + /// # use kalosm_sound::speaker::SpeakerInput; + /// let input = SpeakerInput::new().unwrap(); + /// let stream = input.stream().unwrap(); + /// let _rate = stream.sample_rate(); + /// ``` pub fn stream(self) -> Result { let inner = self.inner.stream(); Ok(SpeakerStream { inner }) } + /// Attempts to obtain a speaker input stream on platforms that do not support speaker capture. + /// + /// # Returns + /// + /// An `Err` containing a message that `SpeakerInput::stream` is not supported on the current platform. + /// + /// # Examples + /// + /// ``` + /// // This example shows that calling `stream` on unsupported platforms yields an error. + /// # use anyhow::Result; + /// # fn try_stream() -> Result<()> { + /// # Err(anyhow::anyhow!("example"))?; // placeholder to make doctest compile when not run + /// # } + /// ``` #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] pub fn stream(self) -> Result { Err(anyhow::anyhow!( @@ -64,6 +117,29 @@ pub struct SpeakerStream { impl Stream for SpeakerStream { type Item = f32; + /// Polls the stream for the next audio sample. + /// + /// # Returns + /// + /// `Poll::Ready(Some(f32))` with the next sample when available, `Poll::Ready(None)` if the stream ended, + /// or `Poll::Pending` if no data is currently available. + /// + /// # Examples + /// + /// ```no_run + /// use std::pin::Pin; + /// use std::task::{Context, Poll, Waker}; + /// // Assume `stream` is a `SpeakerStream` obtained from `SpeakerInput::stream()`. + /// // let mut stream = ...; + /// // let mut pinned = Box::pin(stream); + /// // let waker = futures::task::noop_waker(); + /// // let mut cx = Context::from_waker(&waker); + /// // match Pin::as_mut(&mut pinned).poll_next(&mut cx) { + /// // Poll::Ready(Some(sample)) => println!("sample: {}", sample), + /// // Poll::Ready(None) => println!("stream ended"), + /// // Poll::Pending => println!("no data yet"), + /// // } + /// ``` fn poll_next( self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>, @@ -81,16 +157,49 @@ impl Stream for SpeakerStream { } impl kalosm_sound::AsyncSource for SpeakerStream { + /// Expose this SpeakerStream as an asynchronous stream of audio samples. + /// + /// The returned stream yields `f32` sample values from the underlying speaker input and borrows + /// from `self` for the lifetime of the returned value. + /// + /// # Examples + /// + /// ``` + /// # use futures::stream::StreamExt; + /// # use kalosm_sound::speaker::SpeakerStream; + /// async fn use_stream(mut s: SpeakerStream) { + /// let mut stream = s.as_stream(); + /// // Drive the stream to obtain the next sample (requires an async runtime). + /// let _sample = stream.next().await; + /// } + /// ``` fn as_stream(&mut self) -> impl Stream + '_ { self } - #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] + /// Get the sample rate of the underlying speaker stream in hertz. + /// + /// # Examples + /// + /// ``` + /// let rate = stream.sample_rate(); + /// assert!(rate > 0); + /// ``` fn sample_rate(&self) -> u32 { self.inner.sample_rate() } - #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + /// Report the stream's sample rate on unsupported platforms. + /// + /// On targets other than macOS, Windows, or Linux this method always reports `0` to indicate the sample rate is unavailable. + /// + /// # Examples + /// + /// ``` + /// // On an unsupported platform this should return 0: + /// // let rate = stream.sample_rate(); + /// // assert_eq!(rate, 0); + /// ``` fn sample_rate(&self) -> u32 { 0 } @@ -191,4 +300,4 @@ mod tests { assert!(sample_count > 0, "Should receive some audio samples"); println!("Received {} samples from Windows speaker", sample_count); } -} +} \ No newline at end of file diff --git a/crates/detect/src/app/linux.rs b/crates/detect/src/app/linux.rs index 5a05b34cc..792dbddf3 100644 --- a/crates/detect/src/app/linux.rs +++ b/crates/detect/src/app/linux.rs @@ -17,6 +17,19 @@ pub struct Detector { } impl Default for Detector { + /// Creates a `Detector` with its background task initialized to the default. + + /// + + /// # Examples + + /// + + /// ``` + + /// let _detector = Detector::default(); + + /// ``` fn default() -> Self { Self { background: BackgroundTask::default(), @@ -25,6 +38,26 @@ impl Default for Detector { } impl crate::Observer for Detector { + /// Starts a background detector that periodically scans running processes and invokes `f` for each detected meeting application. + /// + /// The callback `f` will be called with the detected application's name each time the detector finds a matching process. + /// + /// # Parameters + /// + /// - `f`: Callback invoked with the detected application's name. + /// + /// # Examples + /// + /// ``` + /// use detect::app::linux::Detector; + /// + /// let mut detector = Detector::default(); + /// detector.start(|app_name: String| { + /// println!("Detected meeting app: {}", app_name); + /// }); + /// // ...later + /// detector.stop(); + /// ``` fn start(&mut self, f: crate::DetectCallback) { self.background.start(|running, mut rx| async move { let mut interval_timer = interval(Duration::from_secs(5)); @@ -58,6 +91,15 @@ impl crate::Observer for Detector { }); } + /// Stops the detector's background task. + /// + /// # Examples + /// + /// ``` + /// let mut detector = Detector::default(); + /// // start would normally be called before stop in real usage + /// detector.stop(); + /// ``` fn stop(&mut self) { self.background.stop(); } diff --git a/crates/detect/src/browser/linux.rs b/crates/detect/src/browser/linux.rs index f9746cbb5..03f58b0c8 100644 --- a/crates/detect/src/browser/linux.rs +++ b/crates/detect/src/browser/linux.rs @@ -16,6 +16,13 @@ pub struct Detector { } impl Default for Detector { + /// Creates a new `Detector` with a default background task and no previously detected browsers. + /// + /// # Examples + /// + /// ``` + /// let detector = Detector::default(); + /// ``` fn default() -> Self { Self { background: BackgroundTask::default(), @@ -25,6 +32,23 @@ impl Default for Detector { } impl crate::Observer for Detector { + /// Starts a background task that detects common Linux browsers and reports newly observed ones. + /// + /// The detector samples processes every 5 seconds (using `ps aux`) and, for each browser name in + /// `BROWSER_NAMES`, invokes the provided callback exactly once when that browser is first observed + /// running. The callback is called with a message in the format `" running"`. + /// + /// # Parameters + /// + /// - `f`: Callback invoked with a single `String` message when a browser is detected. + /// + /// # Examples + /// + /// ``` + /// let mut detector = crate::browser::linux::Detector::default(); + /// detector.start(|msg| println!("{}", msg)); + /// // The callback will be called asynchronously when a browser from BROWSER_NAMES is observed. + /// ``` fn start(&mut self, f: crate::DetectCallback) { let mut detected_browsers = self.detected_browsers.clone(); @@ -63,6 +87,14 @@ impl crate::Observer for Detector { }); } + /// Stops the detector's background task and clears the set of previously detected browsers. + /// + /// # Examples + /// + /// ``` + /// let mut det = Detector::default(); + /// det.stop(); + /// ``` fn stop(&mut self) { self.background.stop(); self.detected_browsers.clear(); diff --git a/crates/detect/src/mic/linux.rs b/crates/detect/src/mic/linux.rs index 4f2ad5045..4324b910d 100644 --- a/crates/detect/src/mic/linux.rs +++ b/crates/detect/src/mic/linux.rs @@ -7,6 +7,13 @@ pub struct Detector { } impl Default for Detector { + /// Creates a `Detector` initialized with a default background task. + /// + /// # Examples + /// + /// ``` + /// let _ = Detector::default(); + /// ``` fn default() -> Self { Self { background: BackgroundTask::default(), @@ -15,6 +22,28 @@ impl Default for Detector { } impl crate::Observer for Detector { + /// Starts background monitoring for microphone usage and invokes the callback when usage is detected. + /// + /// The detector spawns a background task that checks the system PulseAudio source outputs every 2 seconds. + /// When a microphone source output is present, the provided callback `f` is called with the event name + /// `"microphone_in_use"`. + /// + /// # Parameters + /// + /// - `f`: Callback invoked with a single `String` argument containing the event name when microphone usage is detected. + /// + /// # Examples + /// + /// ``` + /// use crates_detect::mic::linux::Detector; + /// + /// let mut detector = Detector::default(); + /// detector.start(|event| { + /// // handle events such as "microphone_in_use" + /// println!("Event: {}", event); + /// }); + /// detector.stop(); + /// ``` fn start(&mut self, f: crate::DetectCallback) { self.background.start(|running, mut rx| async move { let mut interval_timer = interval(Duration::from_secs(2)); @@ -39,11 +68,33 @@ impl crate::Observer for Detector { }); } + /// Stops the detector's background monitoring task. + /// + /// Terminates any running background task started by `start`. + /// + /// # Examples + /// + /// ``` + /// let mut detector = Detector::default(); + /// detector.stop(); + /// ``` fn stop(&mut self) { self.background.stop(); } } +/// Checks whether any PulseAudio source outputs (applications using the microphone) are active. +/// +/// Runs `pactl list source-outputs short` and returns `true` if the command produced non-empty stdout, +/// indicating one or more active microphone streams. If the command fails or produces empty output, +/// this function returns `false`. +/// +/// # Examples +/// +/// ``` +/// let in_use = is_microphone_in_use(); +/// // `in_use` is `true` if any application is currently using the microphone. +/// ``` fn is_microphone_in_use() -> bool { // Check if any source-outputs exist (applications using microphone) if let Ok(output) = Command::new("pactl") diff --git a/crates/file/examples/checksum.rs b/crates/file/examples/checksum.rs index 929408ef4..a8f141a6e 100644 --- a/crates/file/examples/checksum.rs +++ b/crates/file/examples/checksum.rs @@ -1,6 +1,20 @@ use file::calculate_file_checksum; use std::path::Path; +/// Example program that computes and prints the checksum of a model file. +/// +/// Prints diagnostic information (path, existence, optional size) and the computed checksum +/// for the hard-coded model path used by the example. +/// +/// # Examples +/// +/// ```no_run +/// // Running the example program will print the path, existence, optional size, and checksum. +/// // The example uses a hard-coded model path and is not run as part of doctests. +/// fn main() { +/// crate::main(); +/// } +/// ``` fn main() { let model_path = Path::new("/home/benediktb/.local/share/com.hyprnote.dev/stt/ggml-small-q8_0.bin"); diff --git a/crates/transcribe-whisper-local/examples/show_data_dir.rs b/crates/transcribe-whisper-local/examples/show_data_dir.rs index 6ddbcbc5e..fae7c958e 100644 --- a/crates/transcribe-whisper-local/examples/show_data_dir.rs +++ b/crates/transcribe-whisper-local/examples/show_data_dir.rs @@ -1,5 +1,16 @@ use std::path::PathBuf; +/// Displays the user data directory, the constructed model file path under +/// "com.hyprnote.dev/stt/ggml-small-q8_0.bin", whether that model file exists, +/// and the file size in bytes when it does. +/// +/// # Examples +/// +/// ``` +/// // Run the example program which prints the data directory, model path, +/// // existence, and size (if present). +/// main(); +/// ``` fn main() { let model_path = dirs::data_dir() .unwrap() diff --git a/crates/whisper-local/examples/list_backends.rs b/crates/whisper-local/examples/list_backends.rs index fab269bb7..e48f927f8 100644 --- a/crates/whisper-local/examples/list_backends.rs +++ b/crates/whisper-local/examples/list_backends.rs @@ -1,5 +1,21 @@ use whisper_local::list_ggml_backends; +/// Lists available GGML backends and prints their kind, name, description, and memory stats. +/// +/// # Examples +/// +/// ``` +/// // Run the example binary to print detected backends to stdout. +/// fn main() { +/// let backends = whisper_local::list_ggml_backends(); +/// println!("Available backends:"); +/// for backend in backends { +/// println!(" {}: {} - {} ({} MB free / {} MB total)", +/// backend.kind, backend.name, backend.description, +/// backend.free_memory_mb, backend.total_memory_mb); +/// } +/// } +/// ``` fn main() { let backends = list_ggml_backends(); println!("Available backends:"); diff --git a/crates/whisper-local/examples/test_model.rs b/crates/whisper-local/examples/test_model.rs index ea5d54ddb..ba640ad30 100644 --- a/crates/whisper-local/examples/test_model.rs +++ b/crates/whisper-local/examples/test_model.rs @@ -1,6 +1,16 @@ use whisper_local::Whisper; use std::path::PathBuf; +/// Demonstrates initializing a local Whisper model from a filesystem path and prints basic diagnostics. +/// +/// Prints the model path, whether the file exists, the file size if available, and constructs a `Whisper` instance. +/// +/// # Examples +/// +/// ```no_run +/// // Adjust the hardcoded path in the example file to point to a valid model on your system before running. +/// crate::main(); +/// ``` fn main() { let model_path = PathBuf::from("/home/benediktb/.local/share/com.hyprnote.dev/stt/ggml-small-q8_0.bin"); diff --git a/crates/whisper-local/src/model.rs b/crates/whisper-local/src/model.rs index 387a21c1f..af8df159a 100644 --- a/crates/whisper-local/src/model.rs +++ b/crates/whisper-local/src/model.rs @@ -43,6 +43,25 @@ impl WhisperBuilder { self } + /// Builds a ready-to-use `Whisper` instance by loading the configured model and initializing runtime state. + /// + /// The builder will load the model file at the configured path, attempt to initialize a `WhisperContext` (preferring GPU when available and falling back to CPU), create a `WhisperState`, and return a `Whisper` populated with configured prompts and languages. + /// + /// # Panics + /// + /// - If the configured model path is not set or the model file does not exist. + /// - If both GPU and CPU context initialization fail, or if creating the `WhisperState` fails. + /// + /// # Examples + /// + /// ``` + /// let whisper = Whisper::builder() + /// .model_path("models/ggml-whisper-small.bin".to_string()) + /// .static_prompt("System prompt".to_string()) + /// .build(); + /// + /// // ready to call whisper.transcribe(...) + /// ``` pub fn build(self) -> Whisper { unsafe { Self::suppress_log() }; @@ -426,4 +445,4 @@ mod tests { let segments = whisper.transcribe(&audio).unwrap(); assert!(segments.len() > 0); } -} +} \ No newline at end of file diff --git a/owhisper/owhisper-server/src/commands/run.rs b/owhisper/owhisper-server/src/commands/run.rs index 8e6c41bb5..63e43b356 100644 --- a/owhisper/owhisper-server/src/commands/run.rs +++ b/owhisper/owhisper-server/src/commands/run.rs @@ -13,6 +13,23 @@ pub struct RunArgs { pub dry_run: bool, } +/// Runs the server and real-time audio transcription loop according to the provided command-line arguments. +/// +/// This starts a local server, initializes the selected microphone input, creates a ListenClient that +/// forwards real-time audio to the server, and prints partial and final transcription results to stdout. +/// If `args.dry_run` is true, it instead prints available input devices and returns without starting the server. +/// The function exits the run loop when a shutdown signal is received and aborts the spawned server task before returning. +/// +/// # Examples +/// +/// ``` +/// use tokio::runtime::Runtime; +/// // Construct minimal RunArgs with dry_run to avoid starting servers or audio I/O in the example. +/// let args = RunArgs { model: "QuantizedTiny".into(), config: None, device: None, dry_run: true }; +/// let rt = Runtime::new().unwrap(); +/// rt.block_on(async { handle_run(args).await.unwrap() }); +/// ``` +pub async fn handle_run(args: RunArgs) -> anyhow::Result<()> { pub async fn handle_run(args: RunArgs) -> anyhow::Result<()> { if args.dry_run { print_input_devices(); @@ -120,4 +137,4 @@ fn print_input_devices() { .to_string(); println!("{}", table); -} +} \ No newline at end of file diff --git a/plugins/listener/src/error.rs b/plugins/listener/src/error.rs index f37c3a4e3..8a789110d 100644 --- a/plugins/listener/src/error.rs +++ b/plugins/listener/src/error.rs @@ -29,10 +29,23 @@ pub enum Error { } impl Serialize for Error { + /// Serializes the error as its human-readable string representation. + /// + /// The error is converted with `to_string()` and that string is serialized. + /// + /// # Examples + /// + /// ``` + /// use serde_json::to_string; + /// // construct an example error variant + /// let err = crate::Error::NoneSession; + /// let s = to_string(&err).unwrap(); + /// assert_eq!(s, "\"no session\""); + /// ``` fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { serializer.serialize_str(self.to_string().as_ref()) } -} +} \ No newline at end of file diff --git a/plugins/listener/src/ext.rs b/plugins/listener/src/ext.rs index 0b28ac7d6..153b458b0 100644 --- a/plugins/listener/src/ext.rs +++ b/plugins/listener/src/ext.rs @@ -43,6 +43,21 @@ impl> ListenerPluginExt for T { Ok(hypr_audio::AudioInput::list_mic_devices()) } + /// Retrieve the currently selected microphone device name, if any. + /// + /// Returns `Ok(Some(name))` with the selected device name, `Ok(None)` if no device is selected, or an `Err` if an error occurs. + /// + /// # Examples + /// + /// ```no_run + /// // `plugin` implements the trait providing this method. + /// let current = plugin.get_current_microphone_device().await.unwrap(); + /// if let Some(name) = current { + /// println!("Current mic: {}", name); + /// } else { + /// println!("No microphone selected"); + /// } + /// ``` #[tracing::instrument(skip_all)] async fn get_current_microphone_device(&self) -> Result, crate::Error> { let state: tauri::State<'_, crate::SharedState> = self.state::(); @@ -50,6 +65,24 @@ impl> ListenerPluginExt for T { Ok(s.fsm.get_current_mic_device()) } + /// Sets the active microphone device by name. + /// + /// Dispatches a microphone-change event to the internal state machine to select the given device. + /// + /// # Returns + /// + /// `Ok(())` if the change was dispatched successfully, `Err(crate::Error)` if an error occurred. + /// + /// # Examples + /// + /// ``` + /// // Assumes `api` implements ListenerPluginExt and is available in scope. + /// // This example is illustrative; adapt to your test harness. + /// # async fn run_example>(api: &T) -> Result<(), crate::Error> { + /// api.set_microphone_device("Built-in Microphone").await?; + /// # Ok(()) + /// # } + /// ``` #[tracing::instrument(skip_all)] async fn set_microphone_device( &self, @@ -66,6 +99,21 @@ impl> ListenerPluginExt for T { Ok(()) } + /// Checks whether the current process has permission to capture audio from the microphone. + /// + /// On macOS this queries the system authorization status. On other platforms this attempts to open a microphone input and read a sample to infer access. Returns `Ok(true)` when permission is available, `Ok(false)` when permission is denied or cannot be obtained, and `Err` for underlying OS/IO errors. + /// + /// # Examples + /// + /// ``` + /// # use futures::executor::block_on; + /// # struct Dummy; + /// # mod crate { pub enum Error {} } + /// # impl Dummy { async fn check_microphone_access(&self) -> Result { Ok(true) } } + /// # let plugin = Dummy; + /// let has_access = block_on(plugin.check_microphone_access()).unwrap(); + /// assert!(has_access == true || has_access == false); + /// ``` #[tracing::instrument(skip_all)] async fn check_microphone_access(&self) -> Result { #[cfg(target_os = "macos")] @@ -103,6 +151,20 @@ impl> ListenerPluginExt for T { Ok(hypr_tcc::audio_capture_permission_granted()) } + /// Requests microphone capture permission from the operating system. + /// + /// On macOS this calls the AVFoundation API to prompt the user for microphone access; on other platforms it attempts to open the default microphone stream to trigger or verify permission. The call performs no further side effects beyond initiating or checking the permission request. + /// + /// # Examples + /// + /// ```no_run + /// // Initiate a permission request and propagate any error. + /// listener.request_microphone_access().await?; + /// ``` + /// + /// # Returns + /// + /// `Ok(())` if the permission request was initiated or checked without an internal error, `Err(crate::Error)` if an error occurred while attempting to initiate or verify the request. #[tracing::instrument(skip_all)] async fn request_microphone_access(&self) -> Result<(), crate::Error> { #[cfg(target_os = "macos")] @@ -173,6 +235,17 @@ impl> ListenerPluginExt for T { Ok(()) } + /// Opens the macOS Privacy → Microphone settings pane. + /// + /// Attempts to launch the system Settings app to the Microphone privacy page. + /// Returns `Ok(())` on success, or `Err(crate::Error::IoError)` if spawning or waiting for the `open` process fails. + /// + /// # Examples + /// + /// ```no_run + /// // call from an async context where `self` is available (e.g., inside an impl or test harness) + /// // self.open_microphone_access_settings().await.unwrap(); + /// ``` #[tracing::instrument(skip_all)] async fn open_microphone_access_settings(&self) -> Result<(), crate::Error> { std::process::Command::new("open") @@ -184,6 +257,23 @@ impl> ListenerPluginExt for T { Ok(()) } + /// Opens the macOS Privacy > Audio Capture settings pane. + /// + /// Attempts to launch the system Settings (System Preferences) pane for Audio Capture + /// and waits for the spawned process to exit. + /// + /// # Returns + /// + /// `Ok(())` if the settings process was spawned and waited on successfully, `Err(crate::Error::IoError(_))` if spawning or waiting for the process failed. + /// + /// # Examples + /// + /// ``` + /// # use futures::executor::block_on; + /// # async fn run_example(plugin: &impl ListenerPluginExt) { + /// plugin.open_system_audio_access_settings().await.unwrap(); + /// # } + /// ``` #[tracing::instrument(skip_all)] async fn open_system_audio_access_settings(&self) -> Result<(), crate::Error> { std::process::Command::new("open") @@ -195,6 +285,21 @@ impl> ListenerPluginExt for T { Ok(()) } + /// Get the current finite-state-machine state. + /// + /// # Returns + /// + /// The current FSM state (cloned). + /// + /// # Examples + /// + /// ```no_run + /// // Obtain a listener instance in your application context, then: + /// // let listener = ...; + /// // Use a runtime to await the async call: + /// use futures::executor::block_on; + /// let state = block_on(async { listener.get_state().await }); + /// ``` #[tracing::instrument(skip_all)] async fn get_state(&self) -> crate::fsm::State { let state: tauri::State<'_, crate::SharedState> = self.state::(); @@ -202,6 +307,15 @@ impl> ListenerPluginExt for T { guard.fsm.state().clone() } + /// Report whether the microphone is currently muted. + /// + /// # Examples + /// + /// ``` + /// // In an async context: + /// let muted = plugin.get_mic_muted().await; + /// println!("microphone muted: {}", muted); + /// ``` #[tracing::instrument(skip_all)] async fn get_mic_muted(&self) -> bool { let state: tauri::State<'_, crate::SharedState> = self.state::(); @@ -212,6 +326,16 @@ impl> ListenerPluginExt for T { } } + /// Returns whether the system speaker is currently muted. + /// + /// # Examples + /// + /// ```no_run + /// # async fn doc_example(plugin: &impl crate::ListenerPluginExt) { + /// let muted = plugin.get_speaker_muted().await; + /// assert!(muted == true || muted == false); + /// # } + /// ``` #[tracing::instrument(skip_all)] async fn get_speaker_muted(&self) -> bool { let state: tauri::State<'_, crate::SharedState> = self.state::(); @@ -287,4 +411,4 @@ impl> ListenerPluginExt for T { guard.fsm.handle(&event).await; } } -} +} \ No newline at end of file diff --git a/plugins/listener/src/fsm.rs b/plugins/listener/src/fsm.rs index 46bbe06f0..6801a26d7 100644 --- a/plugins/listener/src/fsm.rs +++ b/plugins/listener/src/fsm.rs @@ -198,6 +198,30 @@ impl Session { } } + /// Initialize and start all per-session audio and processing resources for the given session id. + /// + /// Sets up session state, mute controls, audio input/output streams, AEC and processing pipelines, + /// background tasks for saving and streaming audio, and the listen client integration. On success, + /// the session's background tasks are stored on the Session and begin running. + /// + /// # Parameters + /// + /// * `id` — The session identifier to set up resources for. + /// + /// # Returns + /// + /// `Ok(())` if all required resources were initialized and background tasks were started; an error + /// variant of `crate::Error` if initialization failed (for example missing user id, I/O errors, + /// or failure to create required clients). + /// + /// # Examples + /// + /// ```no_run + /// # async fn example(mut session: crate::Session) -> Result<(), crate::Error> { + /// session.setup_resources("session-123").await?; + /// # Ok(()) + /// # } + /// ``` #[tracing::instrument(skip_all)] async fn setup_resources(&mut self, id: impl Into) -> Result<(), crate::Error> { use tauri_plugin_db::DatabasePluginExt; @@ -643,6 +667,24 @@ async fn setup_listen_client( .build_dual()) } +/// Appends the provided words to the session identified by `session_id`, persists the session, and returns the updated words. +/// +/// If the session does not exist, returns `crate::Error::NoneSession`. Failures while upserting the session are logged but do not change the returned words or cause an error. +/// +/// # Returns +/// +/// The session's full `words` vector after appending the given entries. +/// +/// # Examples +/// +/// ```no_run +/// # use tauri::AppHandle; +/// # async fn demo(app: &AppHandle) -> Result<(), crate::Error> { +/// let new_words = vec![]; +/// let updated = crate::session::update_session(app, "session-id", new_words).await?; +/// println!("Session now has {} words", updated.len()); +/// # Ok(()) } +/// ``` async fn update_session( app: &tauri::AppHandle, session_id: impl Into, @@ -681,6 +723,36 @@ pub enum StateEvent { state(derive(Debug, Clone, PartialEq)) )] impl Session { + /// Handle common state-machine events shared across states. + /// + /// Processes `StateEvent::MicMuted` and `StateEvent::SpeakerMuted` by sending the mute value + /// to the corresponding watch channel (if present) and emitting a session event to the app. + /// Processes `StateEvent::MicChange` by updating the stored microphone device name and, if a + /// session is active, tearing down and reinitializing session resources; setup failures are + /// logged but do not panic. All other events are delegated to the parent state (`Super`). + /// + /// # Parameters + /// + /// - `event`: the incoming `StateEvent` to handle; recognized variants are `MicMuted`, + /// `SpeakerMuted`, and `MicChange`. + /// + /// # Returns + /// + /// `Handled` when the function processes the event here; `Super` to delegate handling to the + /// parent state for all other events. + /// + /// # Examples + /// + /// ```no_run + /// # use tokio::runtime::Runtime; + /// # let rt = Runtime::new().unwrap(); + /// # rt.block_on(async { + /// // `sm` is the state-machine instance; call `common` to handle a mute event. + /// // This example is illustrative and marked `no_run` because constructing a full state + /// // machine is out of scope for the snippet. + /// // sm.common(&StateEvent::MicMuted(true)).await; + /// # }); + /// ``` #[superstate] async fn common(&mut self, event: &StateEvent) -> Response { match event { @@ -818,6 +890,18 @@ impl Session { } } + /// Handle a state transition by emitting the matching session event and updating the session state channel. + /// + /// Logs the transition in debug builds, emits `SessionEvent::RunningActive`, `SessionEvent::RunningPaused`, + /// or `SessionEvent::Inactive` according to `target`, and, if present, forwards the new `target` state on + /// `session_state_tx`. + /// + /// # Examples + /// + /// ```rust,no_run + /// // Given a mutable `session: Session`, notify it of a transition: + /// // session.on_transition(&State::Inactive {}, &State::RunningActive {}); + /// ``` fn on_transition(&mut self, source: &State, target: &State) { #[cfg(debug_assertions)] tracing::info!("transitioned from `{:?}` to `{:?}`", source, target); @@ -860,4 +944,4 @@ impl specta::Type for State { ) -> specta::DataType { specta::datatype::PrimitiveType::String.into() } -} +} \ No newline at end of file