Whole-workspace rustfmt pass picked up while iterating on Mamba GPU backward work. Verified formatting-only via diff sampling; no logic changed. Co-Authored-By: Claude Sonnet 5 <[email protected]>
224 lines
6.4 KiB
Rust
224 lines
6.4 KiB
Rust
//! Text-to-Speech (TTS) synthesis for RustyTorch
|
|
//!
|
|
//! This crate provides comprehensive text-to-speech synthesis capabilities including:
|
|
//!
|
|
//! # Features
|
|
//!
|
|
//! - **Text Processing**: Advanced text normalization, phonemization, and prosody prediction
|
|
//! - **Neural Vocoding**: GPU-accelerated neural vocoders for high-quality speech synthesis
|
|
//! - **Multi-language Support**: Extensible framework for multiple languages and voices
|
|
//! - **Real-time Synthesis**: Optimized for low-latency speech generation
|
|
//!
|
|
//! # Architecture
|
|
//!
|
|
//! The TTS pipeline consists of several stages:
|
|
//!
|
|
//! 1. **Text Processing** (`text` module):
|
|
//! - Text normalization (case, punctuation, abbreviations, numbers)
|
|
//! - Grapheme-to-phoneme (G2P) conversion
|
|
//! - Prosody prediction (pauses, emphasis, intonation)
|
|
//!
|
|
//! 2. **Acoustic Modeling** (`acoustic` module):
|
|
//! - Mel-spectrogram generation from phonemes
|
|
//! - Duration and pitch prediction with variance adaptors
|
|
//! - FastSpeech2 and Tacotron2 models
|
|
//!
|
|
//! 3. **Vocoding** (`vocoder` module):
|
|
//! - Neural vocoders (HiFi-GAN) for high-quality synthesis
|
|
//! - Griffin-Lim algorithm for fast baseline vocoding
|
|
//! - GPU-accelerated waveform generation
|
|
//!
|
|
//! # Example
|
|
//!
|
|
//! ```rust
|
|
//! use rtx_tts::text::{TextPipeline, TextNormalizerConfig};
|
|
//!
|
|
//! # fn main() -> rtx_tts::error::Result<()> {
|
|
//! // Create a text processing pipeline
|
|
//! let pipeline = TextPipeline::default();
|
|
//!
|
|
//! // Process input text
|
|
//! let text = "Hello, world! I have 3 apples.";
|
|
//! let processed = pipeline.process_full(text)?;
|
|
//!
|
|
//! println!("Normalized: {}", processed.normalized);
|
|
//! println!("Phonemes: {}", processed.phoneme_string(true));
|
|
//! # Ok(())
|
|
//! # }
|
|
//! ```
|
|
//!
|
|
//! # Text Normalization
|
|
//!
|
|
//! The text normalizer handles various text transformations:
|
|
//!
|
|
//! ```rust
|
|
//! use rtx_tts::text::{TextNormalizer, TextNormalizerConfig};
|
|
//!
|
|
//! # fn main() -> rtx_tts::error::Result<()> {
|
|
//! let config = TextNormalizerConfig::default();
|
|
//! let normalizer = TextNormalizer::new(config);
|
|
//!
|
|
//! let result = normalizer.normalize("Dr. Smith has 50 dollars and 3 cats")?;
|
|
//! assert_eq!(result, "doctor smith has fifty dollars and three cats");
|
|
//! # Ok(())
|
|
//! # }
|
|
//! ```
|
|
//!
|
|
//! # Phonemization
|
|
//!
|
|
//! Convert text to phonemes using ARPAbet notation:
|
|
//!
|
|
//! ```rust
|
|
//! use rtx_tts::text::{Phonemizer, PhonemizerConfig};
|
|
//!
|
|
//! # fn main() -> rtx_tts::error::Result<()> {
|
|
//! let phonemizer = Phonemizer::default();
|
|
//! let phonemes = phonemizer.g2p("hello world")?;
|
|
//!
|
|
//! // Each word gets its own phoneme sequence
|
|
//! assert_eq!(phonemes.len(), 2);
|
|
//! # Ok(())
|
|
//! # }
|
|
//! ```
|
|
//!
|
|
//! # Prosody Prediction
|
|
//!
|
|
//! Predict prosody markers for natural-sounding speech:
|
|
//!
|
|
//! ```rust
|
|
//! use rtx_tts::text::{Prosody, ProsodyConfig, ProsodyMarker};
|
|
//!
|
|
//! # fn main() -> rtx_tts::error::Result<()> {
|
|
//! let prosody = Prosody::default();
|
|
//! let annotations = prosody.predict("How are you?")?;
|
|
//!
|
|
//! // Questions should have rising intonation
|
|
//! let has_rising = annotations.iter().any(|a| {
|
|
//! a.markers.iter().any(|m| matches!(m, ProsodyMarker::Rising))
|
|
//! });
|
|
//! assert!(has_rising);
|
|
//! # Ok(())
|
|
//! # }
|
|
//! ```
|
|
//!
|
|
//! # Acoustic Modeling
|
|
//!
|
|
//! Convert phoneme sequences to mel spectrograms:
|
|
//!
|
|
//! ```rust,ignore
|
|
//! use rtx_tts::acoustic::{AcousticModel, FastSpeech2, FastSpeech2Config};
|
|
//! use rtx_tensor::{Tensor, Device};
|
|
//!
|
|
//! # fn main() -> rtx_tts::error::Result<()> {
|
|
//! let device = Device::default();
|
|
//! let config = FastSpeech2Config::default();
|
|
//! let model = FastSpeech2::new(config, &device)?;
|
|
//!
|
|
//! // Convert phoneme IDs to mel spectrogram
|
|
//! let phoneme_ids = Tensor::from_slice(&[1, 2, 3, 4], &[1, 4], &device)?;
|
|
//! let mel = model.forward(&phoneme_ids, None)?;
|
|
//! # Ok(())
|
|
//! # }
|
|
//! ```
|
|
//!
|
|
//! # Vocoding
|
|
//!
|
|
//! Convert mel spectrograms to audio waveforms:
|
|
//!
|
|
//! ```rust
|
|
//! use rtx_tts::vocoder::{GriffinLim, GriffinLimConfig, Vocoder};
|
|
//! use rtx_tensor::{Tensor, Device};
|
|
//!
|
|
//! # fn main() -> rtx_tts::error::Result<()> {
|
|
//! let device = Device::cpu();
|
|
//! let config = GriffinLimConfig::default();
|
|
//! let vocoder = GriffinLim::new(config, &device)?;
|
|
//!
|
|
//! // Synthesize audio from mel spectrogram
|
|
//! let mel = Tensor::randn(&[80, 100], &device).unwrap();
|
|
//! let audio = vocoder.synthesize(&mel)?;
|
|
//! # Ok(())
|
|
//! # }
|
|
//! ```
|
|
|
|
#![deny(missing_docs)]
|
|
#![allow(clippy::module_name_repetitions)]
|
|
#![allow(clippy::missing_errors_doc)]
|
|
#![allow(clippy::missing_panics_doc)]
|
|
|
|
pub mod acoustic;
|
|
pub mod error;
|
|
pub mod text;
|
|
pub mod vocoder;
|
|
|
|
// Re-export commonly used types
|
|
pub use acoustic::{
|
|
AcousticModel, FastSpeech2, FastSpeech2Config, MelSpectrogramConfig, Tacotron2,
|
|
Tacotron2Config, VarianceAdaptor, VarianceAdaptorConfig,
|
|
};
|
|
pub use error::{Result, TtsError};
|
|
pub use text::{
|
|
Phoneme, Phonemizer, PhonemizerBackend, PhonemizerConfig, ProcessedText, Prosody,
|
|
ProsodyAnnotation, ProsodyConfig, ProsodyMarker, TextNormalizer, TextNormalizerConfig,
|
|
TextPipeline,
|
|
};
|
|
pub use vocoder::{
|
|
GriffinLim, GriffinLimConfig, HiFiGAN, HiFiGANConfig, HiFiGANGenerator, MelConfig,
|
|
MelSpectrogram, Vocoder, VocoderConfig,
|
|
};
|
|
|
|
/// TTS framework version
|
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
|
|
/// Get the version string
|
|
#[must_use]
|
|
pub fn version() -> &'static str {
|
|
VERSION
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_version() {
|
|
let version = version();
|
|
assert!(!version.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_full_pipeline() {
|
|
let pipeline = TextPipeline::default();
|
|
let result = pipeline.process_full("Hello, world!").unwrap();
|
|
|
|
assert!(!result.original.is_empty());
|
|
assert!(!result.normalized.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_re_exports() {
|
|
// Test that re-exports are working
|
|
let _normalizer = TextNormalizer::default();
|
|
let _phonemizer = Phonemizer::default();
|
|
let _prosody = Prosody::default();
|
|
let _pipeline = TextPipeline::default();
|
|
}
|
|
|
|
#[test]
|
|
fn test_vocoder_re_exports() {
|
|
use rtx_tensor::Device;
|
|
|
|
let device = Device::cpu();
|
|
|
|
// Test vocoder re-exports
|
|
let gl_config = GriffinLimConfig::default();
|
|
let _gl = GriffinLim::new(gl_config, &device).unwrap();
|
|
|
|
let hg_config = HiFiGANConfig::default();
|
|
let _hg = HiFiGAN::new(hg_config, &device).unwrap();
|
|
|
|
let mel_config = MelConfig::default();
|
|
let _mel = MelSpectrogram::new(mel_config, &device).unwrap();
|
|
}
|
|
}
|