Files
ncb-tts-r2/src/tts/voicevox/voicevox.rs
mii443 733646b6b8 refactor: Major overhaul with error handling, resilience patterns, and observability
- Add library configuration to support both lib and binary targets
- Implement unified error handling with NCBError throughout the codebase
- Add circuit breaker pattern for external API calls (Voicevox, GCP TTS)
- Introduce comprehensive performance metrics and monitoring
- Add cache persistence with disk storage support
- Implement retry mechanism with exponential backoff
- Add configuration file support (config.toml) with env var fallback
- Enhance logging with structured tracing (debug, warn, error levels)
- Add extensive unit tests for cache, metrics, and circuit breaker
- Update base64 decoding to use modern API
- Improve API error handling for Voicevox and GCP TTS clients

Breaking changes:
- Function signatures now return Result<T, NCBError> instead of panicking
- Cache key structure modified with serialization support
2025-05-28 01:01:12 +09:00

167 lines
5.6 KiB
Rust

use crate::{errors::NCBError, stream_input::Mp3Request};
use super::structs::{speaker::Speaker, stream::TTSResponse};
const BASE_API_URL: &str = "https://deprecatedapis.tts.quest/v2/";
const STREAM_API_URL: &str = "https://api.tts.quest/v3/voicevox/synthesis";
#[derive(Clone, Debug)]
pub struct VOICEVOX {
pub key: Option<String>,
pub original_api_url: Option<String>,
}
impl VOICEVOX {
#[tracing::instrument]
pub async fn get_styles(&self) -> Result<Vec<(String, i64)>, NCBError> {
let speakers = self.get_speaker_list().await?;
let mut speaker_list = Vec::new();
for speaker in speakers {
for style in speaker.styles {
speaker_list.push((format!("{} - {}", speaker.name, style.name), style.id))
}
}
Ok(speaker_list)
}
#[tracing::instrument]
pub async fn get_speakers(&self) -> Result<Vec<String>, NCBError> {
let speakers = self.get_speaker_list().await?;
let mut speaker_list = Vec::new();
for speaker in speakers {
speaker_list.push(speaker.name)
}
Ok(speaker_list)
}
pub fn new(key: Option<String>, original_api_url: Option<String>) -> Self {
Self {
key,
original_api_url,
}
}
#[tracing::instrument]
async fn get_speaker_list(&self) -> Result<Vec<Speaker>, NCBError> {
let client = reqwest::Client::new();
let request = if let Some(key) = &self.key {
client
.get(format!("{}{}", BASE_API_URL, "voicevox/speakers/"))
.query(&[("key", key)])
} else if let Some(original_api_url) = &self.original_api_url {
client.get(format!("{}/speakers", original_api_url))
} else {
return Err(NCBError::voicevox("No API key or original API URL provided"));
};
let response = request.send().await
.map_err(|e| NCBError::voicevox(format!("Failed to fetch speakers: {}", e)))?;
if !response.status().is_success() {
return Err(NCBError::voicevox(format!(
"API request failed with status: {}",
response.status()
)));
}
response.json().await
.map_err(|e| NCBError::voicevox(format!("Failed to parse speaker list: {}", e)))
}
#[tracing::instrument]
pub async fn synthesize(
&self,
text: String,
speaker: i64,
) -> Result<Vec<u8>, NCBError> {
let key = self.key.as_ref()
.ok_or_else(|| NCBError::voicevox("API key required for synthesis"))?;
let client = reqwest::Client::new();
let response = client
.post(format!("{}{}", BASE_API_URL, "voicevox/audio/"))
.query(&[
("speaker", speaker.to_string()),
("text", text),
("key", key.clone()),
])
.send()
.await
.map_err(|e| NCBError::voicevox(format!("Synthesis request failed: {}", e)))?;
if !response.status().is_success() {
return Err(NCBError::voicevox(format!(
"Synthesis failed with status: {}",
response.status()
)));
}
let body = response.bytes().await
.map_err(|e| NCBError::voicevox(format!("Failed to read response body: {}", e)))?;
Ok(body.to_vec())
}
#[tracing::instrument]
pub async fn synthesize_original(
&self,
text: String,
speaker: i64,
) -> Result<Vec<u8>, NCBError> {
let api_url = self.original_api_url.as_ref()
.ok_or_else(|| NCBError::voicevox("Original API URL required for synthesis"))?;
let client = voicevox_client::Client::new(api_url.clone(), None);
let audio_query = client
.create_audio_query(&text, speaker as i32, None)
.await
.map_err(|e| NCBError::voicevox(format!("Failed to create audio query: {}", e)))?;
tracing::debug!(audio_query = ?audio_query.audio_query, "Generated audio query");
let audio = audio_query.synthesis(speaker as i32, true).await
.map_err(|e| NCBError::voicevox(format!("Audio synthesis failed: {}", e)))?;
Ok(audio.into())
}
#[tracing::instrument]
pub async fn synthesize_stream(
&self,
text: String,
speaker: i64,
) -> Result<Mp3Request, NCBError> {
let key = self.key.as_ref()
.ok_or_else(|| NCBError::voicevox("API key required for stream synthesis"))?;
let client = reqwest::Client::new();
let response = client
.post(STREAM_API_URL)
.query(&[
("speaker", speaker.to_string()),
("text", text),
("key", key.clone()),
])
.send()
.await
.map_err(|e| NCBError::voicevox(format!("Stream synthesis request failed: {}", e)))?;
if !response.status().is_success() {
return Err(NCBError::voicevox(format!(
"Stream synthesis failed with status: {}",
response.status()
)));
}
let body = response.text().await
.map_err(|e| NCBError::voicevox(format!("Failed to read response text: {}", e)))?;
let tts_response: TTSResponse = serde_json::from_str(&body)
.map_err(|e| NCBError::voicevox(format!("Failed to parse TTS response: {}", e)))?;
Ok(Mp3Request::new(reqwest::Client::new(), tts_response.mp3_streaming_url))
}
}