add logging config to audio server

add audio client
change delimiter for mcp tools from "::" to ":" to save tokens
This commit is contained in:
milan
2026-04-09 16:21:58 +02:00
parent 5def79b4ca
commit 5bf98a1efc
17 changed files with 776 additions and 35 deletions
+71
View File
@@ -0,0 +1,71 @@
use std::io::ErrorKind;
use std::path::Path;
use reqwest::Client;
use reqwest::multipart::Form;
use thiserror::Error;
use crate::audio::models::{AudioServerStatus, TranscriptionResponse, VoiceRequest};
use url::Url;
pub struct AudioClient{
client: Client,
authorization: Option<String>,
base_url: Url,
}
pub trait AudioClientTrait {
fn new(base_url: Url) -> Self;
fn from_client(base_url: Url, client: Client) -> Self;
fn with_authorization(self, authorization: String) -> Self;
fn status(&self) -> impl Future<Output = Result<AudioServerStatus, AudioError>>;
fn transcribe(&self, audio_file_path: impl AsRef<Path>) -> impl Future<Output=AudioResult<TranscriptionResponse>>;
fn tts(&self, voice_request: VoiceRequest) -> impl Future<Output=AudioResult<bytes::Bytes>>;
}
#[derive(Debug, Error)]
pub enum AudioError {
#[error(transparent)]
RequestError(#[from] reqwest::Error),
#[error(transparent)]
UrlParseError(#[from] url::ParseError),
#[error(transparent)]
IOError(#[from] std::io::Error),
}
type AudioResult<T> = Result<T, AudioError>;
impl AudioClientTrait for AudioClient {
fn new(base_url: Url) -> Self {
AudioClient { client: Client::new(), authorization: None, base_url }
}
fn from_client(base_url: Url, client: Client) -> Self {
AudioClient { client, authorization: None, base_url }
}
fn with_authorization(mut self, authorization: String) -> Self {
self.authorization = Some(authorization);
self
}
async fn status(&self) -> AudioResult<AudioServerStatus> {
let response = self.client.get(self.base_url.clone()).send().await?.error_for_status()?;
Ok(response.json().await?)
}
async fn transcribe(&self, audio_file_path: impl AsRef<Path>) -> AudioResult<TranscriptionResponse> {
let form = Form::new().file("audio_file", audio_file_path).await?;
let response = self.client.get(self.base_url.join("transcribe")?).multipart(form).send().await?;
Ok(response.json().await?)
}
async fn tts(&self, voice_request: VoiceRequest) -> AudioResult<bytes::Bytes> {
let response = self.client.get(self.base_url.join("tts")?).json(&voice_request).send().await?.error_for_status()?;
Ok(response.bytes().await?)
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod recording;
pub mod client;
pub mod models;
pub use client::{AudioClient, AudioClientTrait};
+60
View File
@@ -0,0 +1,60 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize)]
pub struct AudioServerStatus {
#[serde(rename = "memory_usage")]
pub memory_usage_bytes: usize,
pub piper_model: String,
pub whisper_model: String,
}
//noinspection SpellCheckingInspection
#[derive(Debug, Clone, Deserialize)]
pub struct TranscriptionSegment {
pub id: u32,
pub seek: u32,
pub start: f32,
pub end: f32,
pub text: String,
pub tokens: Vec<u32>,
pub temperature: f32,
pub avg_logprob: f32,
pub compression_ratio: f32,
pub no_speech_prob: f32
}
#[derive(Debug, Clone, Deserialize)]
pub struct TranscriptionResponse {
pub text: String,
pub segments: Vec<TranscriptionSegment>,
pub language: String
}
#[derive(Debug, Clone, Serialize)]
pub struct SynthesisConfig {
pub speaker_id: Option<u32>,
pub length_scale: Option<u32>,
pub noise_scale: Option<u32>,
pub noise_w_scale: Option<u32>,
pub normalize_audio: bool,
pub volume: f32,
}
impl Default for SynthesisConfig {
fn default() -> Self {
SynthesisConfig {
speaker_id: None,
length_scale: None,
noise_scale: None,
noise_w_scale: None,
normalize_audio: true,
volume: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct VoiceRequest {
pub text: String,
pub config: Option<SynthesisConfig>,
}
+197
View File
@@ -0,0 +1,197 @@
use cpal::traits::{DeviceTrait, StreamTrait};
use cpal::{BuildStreamError, Device, SampleFormat, SizedSample, Stream, SupportedStreamConfig};
use std::fmt::Debug;
use std::sync::{Arc, Mutex};
use std::{path, thread};
use std::thread::JoinHandle;
use std::time::Duration;
use hound::WavSpec;
use log::{debug, error, info, trace};
use thiserror::__private18::AsDisplay;
use thiserror::Error;
fn generic_sample_to_i16(sample: impl SizedSample + Into<f64>, format: SampleFormat) -> i16 {
match format {
SampleFormat::I8 => (sample.into() * 2.0) as i16,
SampleFormat::I16 => sample.into() as i16,
SampleFormat::I32 => (sample.into() / 2.0) as i16,
SampleFormat::F32 => (sample.into() * (i16::MAX as f64)) as i16,
_ => panic!("Unsupported sample format {:?}", format),
}
}
pub type SampleArc = Arc<Mutex<Vec<i16>>>;
fn stream_callback<T: SizedSample + Debug + Into<f64>>(
input: &[T],
samples: SampleArc,
sample_format: SampleFormat,
) {
if let Ok(mut guard) = samples.lock() {
for &sample in input {
guard.push(generic_sample_to_i16(sample, sample_format));
}
}
}
fn build_stream<T: SizedSample + Debug + hound::Sample + Send + Into<f64>>(
mic: &Device,
device_config: SupportedStreamConfig,
samples: SampleArc,
sample_format: SampleFormat,
) -> Result<Stream, BuildStreamError> {
mic.build_input_stream(
&device_config.into(),
move |input: &[T], _info| stream_callback::<T>(input, samples.clone(), sample_format),
|e| {
error!("a stream error occurred while trying to record: {:?}", e);
},
None,
)
}
#[derive(Debug)]
pub struct RecordingHandler {
pub samples: SampleArc,
pub spec: WavSpec,
thread_handle: JoinHandle<Result<(), RecordingError>>,
should_stop: Arc<Mutex<bool>>,
}
impl RecordingHandler {
pub fn stop_recording(self) -> Result<(), RecordingError> {
info!("Stopping recording");
*self.should_stop.lock().unwrap() = true;
self.thread_handle.join().unwrap()
}
}
#[derive(Error, Debug)]
pub enum RecordingError {
#[error("unsupported sample format")]
UnsupportedSampleFormat(SampleFormat),
#[error(transparent)]
PlayStreamError(#[from] cpal::PlayStreamError),
#[error(transparent)]
BuildStreamError(#[from] BuildStreamError),
#[error("thread poisoned")]
ThreadPoison,
#[error("recording was already stopped")]
RecordingAlreadyStopped,
}
fn start_recording_blocking_with_parameters(
mic: &Device,
device_config: &SupportedStreamConfig,
sample_format: SampleFormat,
samples: SampleArc,
should_stop: Arc<Mutex<bool>>,
) -> Result<(), RecordingError> {
let stream = match sample_format {
SampleFormat::I8 => {
build_stream::<i8>(mic, device_config.clone(), samples.clone(), sample_format)?
}
SampleFormat::I16 => {
build_stream::<i16>(mic, device_config.clone(), samples.clone(), sample_format)?
}
SampleFormat::I32 => {
build_stream::<i32>(mic, device_config.clone(), samples.clone(), sample_format)?
}
SampleFormat::F32 => {
build_stream::<f32>(mic, device_config.clone(), samples.clone(), sample_format)?
}
sample_format => {
return Err(RecordingError::UnsupportedSampleFormat(sample_format));
}
};
stream.play()?;
loop {
thread::sleep(Duration::from_millis(100));
if let Ok(guard) = should_stop.lock() {
if *guard {
debug!("Stopping recording gracefully");
break;
}
}
}
drop(stream);
Ok(())
}
pub fn start(
microphone: Device,
device_config: SupportedStreamConfig,
) -> Arc<Mutex<Option<RecordingHandler>>> {
let sample_format = device_config.sample_format();
let samples: SampleArc = Arc::new(Mutex::new(Vec::new()));
let recording_should_stop = Arc::new(Mutex::new(false));
info!("started recording");
debug!("sample format: {:?}", sample_format);
let thread_samples = samples.clone();
let thread_recording_stop = recording_should_stop.clone();
let thread_device_config = device_config.clone();
let record_thread_handle = thread::spawn(move ||
start_recording_blocking_with_parameters(
&microphone,
&thread_device_config,
sample_format,
thread_samples,
thread_recording_stop,
)
);
let spec = WavSpec {
channels: device_config.channels(),
sample_rate: device_config.sample_rate(),
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
debug!("device spec: {:?}", spec);
Arc::new(Mutex::new(Some(RecordingHandler {
spec,
samples,
thread_handle: record_thread_handle,
should_stop: recording_should_stop,
})))
}
pub fn stop_and_take_data(handler: Arc<Mutex<Option<RecordingHandler>>>) -> Result<(Vec<i16>, WavSpec), RecordingError> {
let handler = handler.lock().map_err(|_|RecordingError::ThreadPoison)?.take().ok_or(RecordingError::RecordingAlreadyStopped)?;
let samples = handler.samples.clone();
let spec = handler.spec.clone();
handler.stop_recording()?;
Ok((samples.lock().map_err(|_|RecordingError::ThreadPoison)?.to_vec(), spec))
}
pub fn samples_to_wav(
samples: impl IntoIterator<Item = i16>,
spec: &WavSpec,
filepath: impl AsRef<path::Path>
) -> Result<Vec<u8>, hound::Error> {
let wav_bytes = std::io::Cursor::new(Vec::<u8>::new());
trace!("creating a wav file writer to {}", filepath.as_ref().display());
let mut file_writer = hound::WavWriter::create(&filepath, *spec)?;
trace!("writing samples...");
for sample in samples {
file_writer.write_sample(sample)?;
}
trace!("finalizing for file {}", filepath.as_ref().display());
file_writer.finalize()?;
Ok(wav_bytes.into_inner())
}
+1
View File
@@ -1,3 +1,4 @@
pub mod mcp;
pub mod audio;
pub use mcp::chat::AgentChat;
+5 -5
View File
@@ -132,7 +132,7 @@ impl AgentChat {
///
/// # Arguments
///
/// * `name`: must be in the format "mcp_server_name::tool_or_resource_name".
/// * `name`: must be in the format "mcp_server_name:tool_or_resource_name".
///
/// returns:
/// Ok(tuple) => tuple of the server name and the tool or resource name
@@ -141,12 +141,12 @@ impl AgentChat {
/// # Examples
///
/// ```ignore
/// let result = AgentChat::parse_tool_name("example::get_foo".to_string());
/// let result = AgentChat::parse_tool_name("example:get_foo".to_string());
/// assert_eq!(result.unwrap(), ("example".to_string(), "get_foo".to_string()))
/// ```
fn parse_tool_name(name: String) -> Result<(String, String), ChatError> {
let (mcp_server_name, tool_name) = name
.split_once("::")
.split_once(":")
.ok_or(ChatError::FunctionParseError(name.clone()))?;
Ok((mcp_server_name.to_string(), tool_name.to_string()))
@@ -161,7 +161,7 @@ impl AgentChat {
/// # Arguments
///
/// * `mpc_server_data`:
/// * `name`: must be in the format "mcp_server_name::tool_or_resource_name".
/// * `name`: must be in the format "mcp_server_name:tool_or_resource_name".
///
/// returns: Option<&RestrictedTool>
pub fn get_tool(
@@ -352,7 +352,7 @@ mod tests {
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].permission, ToolPermission::Ask);
chat.set_permission("test::echo".to_string(), ToolPermission::Allowed).unwrap();
chat.set_permission("test:echo".to_string(), ToolPermission::Allowed).unwrap();
let tools: Vec<RestrictedTool> = chat.get_all_tools().cloned().collect();
assert_eq!(tools[0].permission, ToolPermission::Allowed);
+2 -2
View File
@@ -32,7 +32,7 @@ fn tool_info_from_mcp_tool(mcp_tool: &rmcp::model::Tool, server_name: &String) -
ToolInfo {
tool_type: ToolType::Function,
function: ToolFunctionInfo {
name: format!("{}::{}", server_name, mcp_tool.name),
name: format!("{}:{}", server_name, mcp_tool.name),
description: mcp_tool
.description
.clone()
@@ -70,7 +70,7 @@ fn tool_info_from_mcp_resource(
ToolInfo {
tool_type: ToolType::Function,
function: ToolFunctionInfo {
name: format!("{}::get_{}", server_name, mcp_resource.name),
name: format!("{}:get_{}", server_name, mcp_resource.name),
description: format!(
"type: {} - {}",
mcp_resource