stt-cli 0.2.1

Speech to text Cli using Groq API and OpenAI API
use anyhow::{anyhow, Context, Result};
use cpal::traits::{DeviceTrait, HostTrait};
use cpal::{
    Device, Host, InputDevices, OutputDevices, SupportedStreamConfig, SupportedStreamConfigRange,
};
use dialoguer::{theme::ColorfulTheme, Select};
use std::collections::HashSet;
use std::sync::Arc;
use tracing::{debug, info, warn};

// Represents an audio device
// #[derive(Debug, Clone, PartialEq, Eq, Hash)] // Added PartialEq, Eq, Hash for potential use in sets/maps
#[derive(Clone)]
pub struct AudioDevice {
    pub name: String,
    pub cpal_device: Device, // Store the actual cpal::Device
    pub is_input: bool,      // Flag to distinguish device type
                             // We could add host_id if needed for multi-host systems
}

impl AudioDevice {
    // pub fn new(name: String, is_input: bool) -> Self {
    //     AudioDevice { name, is_input }
    // }

    pub fn name(&self) -> String {
        self.name.clone()
    }

    pub fn is_input(&self) -> bool {
        self.is_input
    }

    // Private helper to create AudioDevice from cpal::Device
    fn from_cpal_device(device: &Device, is_input: bool) -> Result<Self> {
        Ok(AudioDevice {
            name: device.name().context("Failed to get device name")?,
            cpal_device: device.clone(),
            is_input,
        })
    }

    // Method to get the default input device
    pub fn default_input() -> Result<Arc<Self>> {
        let host = cpal::default_host();
        let device = host
            .default_input_device()
            .ok_or_else(|| anyhow!("No default input device available"))?;
        Ok(Arc::new(Self::from_cpal_device(&device, true)?))
    }

    // Method to get the default output device
    pub fn default_output() -> Result<Arc<Self>> {
        let host = cpal::default_host();
        let device = host
            .default_output_device()
            .ok_or_else(|| anyhow!("No default output device available"))?;
        Ok(Arc::new(Self::from_cpal_device(&device, false)?))
    }

    // Method to list available input devices
    pub fn list_input_devices() -> Result<Vec<Arc<Self>>> {
        let host = cpal::default_host();
        Self::list_devices_internal(host.input_devices()?, true)
    }

    // Method to list available output devices
    pub fn list_output_devices() -> Result<Vec<Arc<Self>>> {
        let host = cpal::default_host();
        Self::list_devices_internal(host.output_devices()?, false)
    }

    // Internal helper for listing devices
    fn list_devices_internal<I>(devices: I, is_input: bool) -> Result<Vec<Arc<Self>>>
    where
        I: Iterator<Item = Device>,
    {
        let mut result = Vec::new();
        for device in devices {
            match Self::from_cpal_device(&device, is_input) {
                Ok(audio_device) => result.push(Arc::new(audio_device)),
                Err(e) => warn!("Failed to process device: {}", e),
            }
        }
        Ok(result)
    }

    // Instance method to get the actual CPAL device and its config
    // This finds the cpal::Device matching the stored name.
    pub async fn get_cpal_device_and_config(&self) -> Result<(Device, SupportedStreamConfig)> {
        let host = cpal::default_host();

        // Try to find the device by name
        let devices = if self.is_input {
            host.input_devices()?
        } else {
            host.output_devices()?
        };

        for device in devices {
            let device_name = device.name()?;
            if device_name == self.name {
                // Found the device, now get its default config
                let config = device
                    .default_input_config()
                    .or_else(|_| device.default_output_config())
                    .context("Failed to get device config")?;

                debug!("Found device '{}' with config: {:?}", self.name, config);
                return Ok((device, config));
            }
        }

        // If we get here, we couldn't find the device
        Err(anyhow!("Device '{}' not found", self.name))
    }
}

/// Prompts the user to select an audio input device from a list.
pub fn select_audio_device(host: &Host) -> Result<Arc<AudioDevice>> {
    let devices =
        AudioDevice::list_input_devices().context("Failed to list input devices for selection")?;

    if devices.is_empty() {
        return Err(anyhow!("No input devices available to select"));
    }

    let device_names: Vec<String> = devices.iter().map(|d| d.name.clone()).collect();

    info!("Please select an audio input device:");

    let selection = Select::with_theme(&ColorfulTheme::default())
        .items(&device_names)
        .default(0)
        .interact()
        .context("Failed to get user selection")?;

    info!("Selected device: {}", device_names[selection]);
    Ok(devices[selection].clone())
}

/// Finds a specific audio device by name (case-insensitive comparison).
pub fn find_device_by_name(host: &Host, name: &str) -> Result<Arc<AudioDevice>> {
    debug!("Searching for device: '{}'", name);
    let lower_name = name.to_lowercase();

    if let Ok(devices) = AudioDevice::list_input_devices() {
        if let Some(device) = devices
            .into_iter()
            .find(|d| d.name.to_lowercase() == lower_name)
        {
            info!("Found input device: {}", device.name);
            return Ok(device);
        }
    }

    warn!("Device '{}' not found.", name);
    Err(anyhow!("Device '{}' not found", name))
}

/// Gets and formats the capabilities of a specific CPAL device.
pub fn get_device_capabilities(device: &Device) -> Result<String> {
    let device_name = device.name().unwrap_or_else(|_| "Unknown".to_string());
    let mut capabilities = format!("Capabilities for device: {}\n", device_name);

    append_configs(&mut capabilities, device.supported_input_configs(), "Input")?;
    append_configs(
        &mut capabilities,
        device.supported_output_configs(),
        "Output",
    )?;

    Ok(capabilities)
}

fn append_configs(
    caps: &mut String,
    configs: Result<
        impl Iterator<Item = SupportedStreamConfigRange>,
        cpal::SupportedStreamConfigsError,
    >,
    config_type: &str,
) -> Result<()> {
    match configs {
        Ok(configs) => {
            let mut unique_configs = HashSet::new();
            for config in configs {
                let config_str = format!(
                    "  {} Channels: {}, Sample Rate: {}-{} Hz, Format: {:?}\n",
                    config_type,
                    config.channels(),
                    config.min_sample_rate().0,
                    config.max_sample_rate().0,
                    config.sample_format()
                );
                unique_configs.insert(config_str);
            }

            let mut sorted_configs: Vec<_> = unique_configs.into_iter().collect();
            sorted_configs.sort();
            for config in sorted_configs {
                caps.push_str(&config);
            }
            Ok(())
        }
        Err(e) => {
            caps.push_str(&format!("  Error getting {} configs: {}\n", config_type, e));
            Ok(())
        }
    }
}

// Lists all audio devices (input and output)
pub fn list_audio_devices() -> Result<Vec<Arc<AudioDevice>>> {
    info!("Listing available audio devices...");
    let mut devices = Vec::new();

    match AudioDevice::list_input_devices() {
        Ok(inputs) => devices.extend(inputs),
        Err(e) => warn!("Failed to list input devices: {}", e),
    }

    match AudioDevice::list_output_devices() {
        Ok(outputs) => devices.extend(outputs),
        Err(e) => warn!("Failed to list output devices: {}", e),
    }

    if devices.is_empty() {
        warn!("No audio devices found");
        Err(anyhow!("No audio devices found"))
    } else {
        debug!("Found {} audio devices", devices.len());
        Ok(devices)
    }
}