add logging config to audio server
add audio client change delimiter for mcp tools from "::" to ":" to save tokens
This commit is contained in:
+5
-2
@@ -5,7 +5,7 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
ollama-rs = {version = "0.3.4", features = ["macros", "headers"]}
|
||||
reqwest = "0.13.2"
|
||||
reqwest = { version = "0.13.2", features = ["stream", "multipart", "form"] }
|
||||
tokio = { version = "1.50.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
rmcp = {version="1.3.0", features = ["transport-streamable-http-client-reqwest", "reqwest", "client", "auth", "transport-child-process"]}
|
||||
log = {version = "0.4.29"}
|
||||
@@ -13,4 +13,7 @@ env_logger = "0.11.10"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
thiserror = "2.0.17"
|
||||
url = "2.5.8"
|
||||
rand = "0.10.0"
|
||||
rand = "0.10.0"
|
||||
cpal = "0.17.3"
|
||||
hound = "3.5.1"
|
||||
bytes = "1.11.1"
|
||||
@@ -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?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod recording;
|
||||
pub mod client;
|
||||
pub mod models;
|
||||
pub use client::{AudioClient, AudioClientTrait};
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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(
|
||||
µphone,
|
||||
&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,3 +1,4 @@
|
||||
pub mod mcp;
|
||||
pub mod audio;
|
||||
|
||||
pub use mcp::chat::AgentChat;
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user