fix(proxy-engine): improve inbound SIP routing diagnostics and enrich leg media state reporting

This commit is contained in:
2026-04-14 20:19:34 +00:00
parent 0d82a626b5
commit 88768f0586
46 changed files with 555689 additions and 107 deletions
+80
View File
@@ -0,0 +1,80 @@
use crate::G2PError;
use bincode::error::DecodeError;
use ndarray::ShapeError;
use ort::Error as OrtError;
use std::{
error::Error,
fmt::{Debug, Display, Formatter, Result as FmtResult},
io::Error as IoError,
time::SystemTimeError,
};
#[derive(Debug)]
pub enum KokoroError {
Decode(DecodeError),
G2P(G2PError),
Io(IoError),
ModelReleased,
Ort(OrtError),
Send(String),
Shape(ShapeError),
SystemTime(SystemTimeError),
VoiceNotFound(String),
VoiceVersionInvalid(String),
}
impl Display for KokoroError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "KokoroError: ")?;
match self {
Self::Decode(e) => Display::fmt(e, f),
Self::G2P(e) => Display::fmt(e, f),
Self::Io(e) => Display::fmt(e, f),
Self::Ort(e) => Display::fmt(e, f),
Self::ModelReleased => write!(f, "ModelReleased"),
Self::Send(e) => Display::fmt(e, f),
Self::Shape(e) => Display::fmt(e, f),
Self::SystemTime(e) => Display::fmt(e, f),
Self::VoiceNotFound(name) => write!(f, "VoiceNotFound({})", name),
Self::VoiceVersionInvalid(msg) => write!(f, "VoiceVersionInvalid({})", msg),
}
}
}
impl Error for KokoroError {}
impl From<IoError> for KokoroError {
fn from(value: IoError) -> Self {
Self::Io(value)
}
}
impl From<DecodeError> for KokoroError {
fn from(value: DecodeError) -> Self {
Self::Decode(value)
}
}
impl From<OrtError> for KokoroError {
fn from(value: OrtError) -> Self {
Self::Ort(value)
}
}
impl From<G2PError> for KokoroError {
fn from(value: G2PError) -> Self {
Self::G2P(value)
}
}
impl From<ShapeError> for KokoroError {
fn from(value: ShapeError) -> Self {
Self::Shape(value)
}
}
impl From<SystemTimeError> for KokoroError {
fn from(value: SystemTimeError) -> Self {
Self::SystemTime(value)
}
}
+321
View File
@@ -0,0 +1,321 @@
/// 文本到国际音标的转换
mod v10;
mod v11;
use super::PinyinError;
use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese};
#[cfg(feature = "use-cmudict")]
use cmudict_fast::{Cmudict, Error as CmudictError};
use pinyin::ToPinyin;
use regex::{Captures, Error as RegexError, Regex};
use std::{
error::Error,
fmt::{Display, Formatter, Result as FmtResult},
};
#[derive(Debug)]
pub enum G2PError {
#[cfg(feature = "use-cmudict")]
CmudictError(CmudictError),
EnptyData,
#[cfg(not(feature = "use-cmudict"))]
Nul(std::ffi::NulError),
Pinyin(PinyinError),
Regex(RegexError),
#[cfg(not(feature = "use-cmudict"))]
Utf8(std::str::Utf8Error),
}
impl Display for G2PError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "G2PError: ")?;
match self {
#[cfg(feature = "use-cmudict")]
Self::CmudictError(e) => Display::fmt(e, f),
Self::EnptyData => Display::fmt("EmptyData", f),
#[cfg(not(feature = "use-cmudict"))]
Self::Nul(e) => Display::fmt(e, f),
Self::Pinyin(e) => Display::fmt(e, f),
Self::Regex(e) => Display::fmt(e, f),
#[cfg(not(feature = "use-cmudict"))]
Self::Utf8(e) => Display::fmt(e, f),
}
}
}
impl Error for G2PError {}
impl From<PinyinError> for G2PError {
fn from(value: PinyinError) -> Self {
Self::Pinyin(value)
}
}
impl From<RegexError> for G2PError {
fn from(value: RegexError) -> Self {
Self::Regex(value)
}
}
#[cfg(feature = "use-cmudict")]
impl From<CmudictError> for G2PError {
fn from(value: CmudictError) -> Self {
Self::CmudictError(value)
}
}
#[cfg(not(feature = "use-cmudict"))]
impl From<std::ffi::NulError> for G2PError {
fn from(value: std::ffi::NulError) -> Self {
Self::Nul(value)
}
}
#[cfg(not(feature = "use-cmudict"))]
impl From<std::str::Utf8Error> for G2PError {
fn from(value: std::str::Utf8Error) -> Self {
Self::Utf8(value)
}
}
fn word2ipa_zh(word: &str) -> Result<String, G2PError> {
let iter = word.chars().map(|i| match i.to_pinyin() {
None => Ok(i.to_string()),
Some(p) => v10::py2ipa(p.with_tone_num_end()),
});
let mut result = String::new();
for i in iter {
result.push_str(&i?);
}
Ok(result)
}
#[cfg(feature = "use-cmudict")]
fn word2ipa_en(word: &str) -> Result<String, G2PError> {
use super::{arpa_to_ipa, letters_to_ipa};
use std::{
io::{Error as IoError, ErrorKind},
str::FromStr,
sync::LazyLock,
};
fn get_cmudict<'a>() -> Result<&'a Cmudict, CmudictError> {
static CMUDICT: LazyLock<Result<Cmudict, CmudictError>> =
LazyLock::new(|| Cmudict::from_str(include_str!("../dict/cmudict.dict")));
CMUDICT.as_ref().map_err(|i| match i {
CmudictError::IoErr(e) => CmudictError::IoErr(IoError::new(ErrorKind::Other, e)),
CmudictError::InvalidLine(e) => CmudictError::InvalidLine(*e),
CmudictError::RuleParseError(e) => CmudictError::RuleParseError(e.clone()),
})
}
if word.chars().count() < 4 && word.chars().all(|c| c.is_ascii_uppercase()) {
return Ok(letters_to_ipa(word));
}
let dict = get_cmudict()?;
let upper = word.to_ascii_uppercase();
let lower = word.to_ascii_lowercase();
let Some(rules) = dict
.get(word)
.or_else(|| dict.get(&upper))
.or_else(|| dict.get(&lower))
else {
return Ok(letters_to_ipa(word));
};
if rules.is_empty() {
return Ok(word.to_owned());
}
let i = rand::random_range(0..rules.len());
let result = rules[i]
.pronunciation()
.iter()
.map(|i| arpa_to_ipa(&i.to_string()).unwrap_or_default())
.collect::<String>();
Ok(result)
}
#[cfg(not(feature = "use-cmudict"))]
fn word2ipa_en(word: &str) -> Result<String, G2PError> {
use super::letters_to_ipa;
use std::{
ffi::{CStr, CString, c_char},
sync::Once,
};
if word.chars().count() < 4 && word.chars().all(|c| c.is_ascii_uppercase()) {
return Ok(letters_to_ipa(word));
}
unsafe extern "C" {
fn TextToPhonemes(text: *const c_char) -> *const ::std::os::raw::c_char;
fn Initialize(data_dictlist: *const c_char);
}
unsafe {
static INIT: Once = Once::new();
INIT.call_once(|| {
static DATA: &[u8] = include_bytes!("../dict/espeak.dict");
Initialize(DATA.as_ptr() as _);
});
let word = CString::new(word.to_lowercase())?.into_raw() as *const c_char;
let res = TextToPhonemes(word);
Ok(CStr::from_ptr(res).to_str()?.to_string())
}
}
fn to_half_shape(text: &str) -> String {
let mut result = String::with_capacity(text.len() * 2); // 预分配合理空间
let chars = text.chars().peekable();
for c in chars {
match c {
// 处理需要后看的情况
'«' | '《' => result.push('“'),
'»' | '》' => result.push('”'),
'' => result.push('('),
'' => result.push(')'),
// 简单替换规则
'、' | '' => result.push(','),
'。' => result.push('.'),
'' => result.push('!'),
'' => result.push(':'),
'' => result.push(';'),
'' => result.push('?'),
// 默认字符
_ => result.push(c),
}
}
// 清理多余空格并返回
result
}
fn num_repr(text: &str) -> Result<String, G2PError> {
let regex = Regex::new(r#"\d+(\.\d+)?"#)?;
Ok(regex
.replace(text, |caps: &Captures| {
let text = &caps[0];
if let Ok(num) = text.parse::<f64>() {
num.to_chinese(
ChineseVariant::Traditional,
ChineseCase::Lower,
ChineseCountMethod::Low,
)
.map_or(text.to_owned(), |i| i)
} else if let Ok(num) = text.parse::<i64>() {
num.to_chinese(
ChineseVariant::Traditional,
ChineseCase::Lower,
ChineseCountMethod::Low,
)
.map_or(text.to_owned(), |i| i)
} else {
text.to_owned()
}
})
.to_string())
}
pub fn g2p(text: &str, use_v11: bool) -> Result<String, G2PError> {
let text = num_repr(text)?;
let sentence_pattern = Regex::new(
r#"([\u4E00-\u9FFF]+)|([,。:·?、!《》()【】〖〗〔〕“”‘’〈〉…— ]+)|([\u0000-\u00FF]+)+"#,
)?;
let en_word_pattern = Regex::new("\\w+|\\W+")?;
let jieba = jieba_rs::Jieba::new();
let mut result = String::new();
for i in sentence_pattern.captures_iter(&text) {
match (i.get(1), i.get(2), i.get(3)) {
(Some(text), _, _) => {
let text = to_half_shape(text.as_str());
if use_v11 {
if !result.is_empty() && !result.ends_with(' ') {
result.push(' ');
}
result.push_str(&v11::g2p(&text, true));
result.push(' ');
} else {
for i in jieba.cut(&text, true) {
result.push_str(&word2ipa_zh(i)?);
result.push(' ');
}
}
}
(_, Some(text), _) => {
let text = to_half_shape(text.as_str());
result = result.trim_end().to_string();
result.push_str(&text);
result.push(' ');
}
(_, _, Some(text)) => {
for i in en_word_pattern.captures_iter(text.as_str()) {
let c = (i[0]).chars().next().unwrap_or_default();
if c == '\''
|| c == '_'
|| c == '-'
|| c.is_ascii_lowercase()
|| c.is_ascii_uppercase()
{
let i = &i[0];
if result.trim_end().ends_with(['.', ',', '!', '?'])
&& !result.ends_with(' ')
{
result.push(' ');
}
result.push_str(&word2ipa_en(i)?);
} else if c == ' ' && result.ends_with(' ') {
result.push_str((i[0]).trim_start());
} else {
result.push_str(&i[0]);
}
}
}
_ => (),
};
}
Ok(result.trim().to_string())
}
#[cfg(test)]
mod tests {
#[cfg(not(feature = "use-cmudict"))]
#[test]
fn test_word2ipa_en() -> Result<(), super::G2PError> {
use super::word2ipa_en;
// println!("{:?}", espeak_rs::text_to_phonemes("days", "en", None, true, false));
assert_eq!("kjˌuːkjˈuː", word2ipa_en("qq")?);
assert_eq!("həlˈəʊ", word2ipa_en("hello")?);
assert_eq!("wˈɜːld", word2ipa_en("world")?);
assert_eq!("ˈapəl", word2ipa_en("apple")?);
assert_eq!("tʃˈɪldɹɛn", word2ipa_en("children")?);
assert_eq!("ˈaʊə", word2ipa_en("hour")?);
assert_eq!("dˈeɪz", word2ipa_en("days")?);
Ok(())
}
#[cfg(feature = "use-cmudict")]
#[test]
fn test_word2ipa_en_is_case_insensitive_for_dictionary_words() -> Result<(), super::G2PError> {
use super::word2ipa_en;
assert_eq!(word2ipa_en("Welcome")?, word2ipa_en("welcome")?);
Ok(())
}
#[test]
fn test_g2p() -> Result<(), super::G2PError> {
use super::g2p;
assert_eq!("ni↓xau↓ ʂɻ↘ʨje↘", g2p("你好世界", false)?);
assert_eq!("ㄋㄧ2ㄏㄠ3/ㄕ十4ㄐㄝ4", g2p("你好世界", true)?);
Ok(())
}
}
+62
View File
@@ -0,0 +1,62 @@
use crate::{G2PError, pinyin_to_ipa};
fn retone(p: &str) -> String {
let chars: Vec<char> = p.chars().collect();
let mut result = String::with_capacity(p.len());
let mut i = 0;
while i < chars.len() {
match () {
// 三声调优先处理
_ if i + 2 < chars.len()
&& chars[i] == '˧'
&& chars[i + 1] == '˩'
&& chars[i + 2] == '˧' =>
{
result.push('↓');
i += 3;
}
// 二声调
_ if i + 1 < chars.len() && chars[i] == '˧' && chars[i + 1] == '˥' => {
result.push('↗');
i += 2;
}
// 四声调
_ if i + 1 < chars.len() && chars[i] == '˥' && chars[i + 1] == '˩' => {
result.push('↘');
i += 2;
}
// 一声调
_ if chars[i] == '˥' => {
result.push('→');
i += 1;
}
// 组合字符替换(ɻ̩ 和 ɱ̩)
_ if !(i + 1 >= chars.len() || chars[i+1] != '\u{0329}' || chars[i] != '\u{027B}' && chars[i] != '\u{0271}') =>
{
result.push('ɨ');
i += 2;
}
// 默认情况
_ => {
result.push(chars[i]);
i += 1;
}
}
}
assert!(
!result.contains('\u{0329}'),
"Unexpected combining mark in: {}",
result
);
result
}
pub(super) fn py2ipa(py: &str) -> Result<String, G2PError> {
pinyin_to_ipa(py)?
.first()
.map_or(Err(G2PError::EnptyData), |i| {
Ok(i.iter().map(|i| retone(i)).collect::<String>())
})
}
File diff suppressed because it is too large Load Diff
+83
View File
@@ -0,0 +1,83 @@
mod error;
mod g2p;
mod stream;
mod synthesizer;
mod tokenizer;
mod transcription;
mod voice;
use {
bincode::{config::standard, decode_from_slice},
ort::{execution_providers::CUDAExecutionProvider, session::Session},
std::{collections::HashMap, path::Path, sync::Arc, time::Duration},
tokio::{fs::read, sync::Mutex},
};
pub use {error::*, g2p::*, stream::*, tokenizer::*, transcription::*, voice::*};
pub struct KokoroTts {
model: Arc<Mutex<Session>>,
voices: Arc<HashMap<String, Vec<Vec<Vec<f32>>>>>,
}
impl KokoroTts {
pub async fn new<P: AsRef<Path>>(model_path: P, voices_path: P) -> Result<Self, KokoroError> {
let voices = read(voices_path).await?;
let (voices, _) = decode_from_slice(&voices, standard())?;
let model = Session::builder()?
.with_execution_providers([CUDAExecutionProvider::default().build()])?
.commit_from_file(model_path)?;
Ok(Self {
model: Arc::new(model.into()),
voices,
})
}
pub async fn new_from_bytes<B>(model: B, voices: B) -> Result<Self, KokoroError>
where
B: AsRef<[u8]>,
{
let (voices, _) = decode_from_slice(voices.as_ref(), standard())?;
let model = Session::builder()?
.with_execution_providers([CUDAExecutionProvider::default().build()])?
.commit_from_memory(model.as_ref())?;
Ok(Self {
model: Arc::new(model.into()),
voices,
})
}
pub async fn synth<S>(&self, text: S, voice: Voice) -> Result<(Vec<f32>, Duration), KokoroError>
where
S: AsRef<str>,
{
let name = voice.get_name();
let pack = self
.voices
.get(name)
.ok_or(KokoroError::VoiceNotFound(name.to_owned()))?;
synthesizer::synth(Arc::downgrade(&self.model), text, pack, voice).await
}
pub fn stream<S>(&self, voice: Voice) -> (SynthSink<S>, SynthStream)
where
S: AsRef<str> + Send + 'static,
{
let voices = Arc::downgrade(&self.voices);
let model = Arc::downgrade(&self.model);
start_synth_session(voice, move |text, voice| {
let voices = voices.clone();
let model = model.clone();
async move {
let name = voice.get_name();
let voices = voices.upgrade().ok_or(KokoroError::ModelReleased)?;
let pack = voices
.get(name)
.ok_or(KokoroError::VoiceNotFound(name.to_owned()))?;
synthesizer::synth(model, text, pack, voice).await
}
})
}
}
+157
View File
@@ -0,0 +1,157 @@
use {
crate::{KokoroError, Voice},
futures::{Sink, SinkExt, Stream},
pin_project::pin_project,
std::{
pin::Pin,
task::{Context, Poll},
time::Duration,
},
tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
};
struct Request<S> {
voice: Voice,
text: S,
}
struct Response {
data: Vec<f32>,
took: Duration,
}
/// 语音合成流
///
/// 该结构体用于通过流式合成来处理更长的文本。它实现了`Stream` trait,可以用于异步迭代合成后的音频数据。
#[pin_project]
pub struct SynthStream {
#[pin]
rx: UnboundedReceiver<Response>,
}
impl Stream for SynthStream {
type Item = (Vec<f32>, Duration);
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.project().rx)
.poll_recv(cx)
.map(|i| i.map(|Response { data, took }| (data, took)))
}
}
/// 语音合成发送端
///
/// 该结构体用于发送语音合成请求。它实现了`Sink` trait,可以用于异步发送合成请求。
#[pin_project]
pub struct SynthSink<S> {
tx: UnboundedSender<Request<S>>,
voice: Voice,
}
impl<S> SynthSink<S> {
/// 设置语音名称
///
/// 该方法用于设置要合成的语音名称。
///
/// # 参数
///
/// * `voice_name` - 语音名称,用于选择要合成的语音。
///
/// # 示例
///
/// ```rust
/// use kokoro_tts::{KokoroTts, Voice};
///
/// #[tokio::main]
/// async fn main() {
/// let Ok(tts) = KokoroTts::new("../kokoro-v1.0.int8.onnx", "../voices.bin").await else {
/// return;
/// };
/// // speed: 1.0
/// let (mut sink, _) = tts.stream::<&str>(Voice::ZfXiaoxiao(1.0));
/// // speed: 1.8
/// sink.set_voice(Voice::ZmYunxi(1.8));
/// }
/// ```
///
pub fn set_voice(&mut self, voice: Voice) {
self.voice = voice
}
/// 发送合成请求
///
/// 该方法用于发送语音合成请求。
///
/// # 参数
///
/// * `text` - 要合成的文本内容。
///
/// # 返回值
///
/// 如果发送成功,将返回`Ok(())`;如果发送失败,将返回一个`KokoroError`类型的错误。
///
/// # 示例
///
/// ```rust
/// use kokoro_tts::{KokoroTts, Voice};
///
/// #[tokio::main]
/// async fn main() {
/// let Ok(tts) = KokoroTts::new("../kokoro-v1.1-zh.onnx", "../voices-v1.1-zh.bin").await else {
/// return;
/// };
/// let (mut sink, _) =tts.stream(Voice::Zf003(2));
/// let _ = sink.synth("hello world.").await;
/// }
/// ```
///
pub async fn synth(&mut self, text: S) -> Result<(), KokoroError> {
self.send((self.voice, text)).await
}
}
impl<S> Sink<(Voice, S)> for SynthSink<S> {
type Error = KokoroError;
fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn start_send(self: Pin<&mut Self>, (voice, text): (Voice, S)) -> Result<(), Self::Error> {
self.tx
.send(Request { voice, text })
.map_err(|e| KokoroError::Send(e.to_string()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
}
pub(super) fn start_synth_session<F, R, S>(
voice: Voice,
synth_request_callback: F,
) -> (SynthSink<S>, SynthStream)
where
F: Fn(S, Voice) -> R + Send + 'static,
R: Future<Output = Result<(Vec<f32>, Duration), KokoroError>> + Send,
S: AsRef<str> + Send + 'static,
{
let (tx, mut rx) = unbounded_channel::<Request<S>>();
let (tx2, rx2) = unbounded_channel();
tokio::spawn(async move {
while let Some(req) = rx.recv().await {
let (data, took) = synth_request_callback(req.text, req.voice).await?;
tx2.send(Response { data, took })
.map_err(|e| KokoroError::Send(e.to_string()))?;
}
Ok::<_, KokoroError>(())
});
(SynthSink { tx, voice }, SynthStream { rx: rx2 })
}
+123
View File
@@ -0,0 +1,123 @@
use {
crate::{KokoroError, Voice, g2p, get_token_ids},
ndarray::Array,
ort::{
inputs,
session::{RunOptions, Session},
value::TensorRef,
},
std::{
cmp::min,
sync::Weak,
time::{Duration, SystemTime},
},
tokio::sync::Mutex,
};
async fn synth_v10<P, S>(
model: Weak<Mutex<Session>>,
phonemes: S,
pack: P,
speed: f32,
) -> Result<(Vec<f32>, Duration), KokoroError>
where
P: AsRef<Vec<Vec<Vec<f32>>>>,
S: AsRef<str>,
{
let model = model.upgrade().ok_or(KokoroError::ModelReleased)?;
let phonemes = get_token_ids(phonemes.as_ref(), false);
let phonemes = Array::from_shape_vec((1, phonemes.len()), phonemes)?;
let ref_s = pack.as_ref()[phonemes.len() - 1]
.first()
.cloned()
.unwrap_or_default();
let style = Array::from_shape_vec((1, ref_s.len()), ref_s)?;
let speed = Array::from_vec(vec![speed]);
let options = RunOptions::new()?;
let mut model = model.lock().await;
let t = SystemTime::now();
let kokoro_output = model
.run_async(
inputs![
"tokens" => TensorRef::from_array_view(&phonemes)?,
"style" => TensorRef::from_array_view(&style)?,
"speed" => TensorRef::from_array_view(&speed)?,
],
&options,
)?
.await?;
let elapsed = t.elapsed()?;
let (_, audio) = kokoro_output["audio"].try_extract_tensor::<f32>()?;
Ok((audio.to_owned(), elapsed))
}
async fn synth_v11<P, S>(
model: Weak<Mutex<Session>>,
phonemes: S,
pack: P,
speed: i32,
) -> Result<(Vec<f32>, Duration), KokoroError>
where
P: AsRef<Vec<Vec<Vec<f32>>>>,
S: AsRef<str>,
{
let model = model.upgrade().ok_or(KokoroError::ModelReleased)?;
let mut phonemes = get_token_ids(phonemes.as_ref(), true);
let mut ret = Vec::new();
let mut elapsed = Duration::ZERO;
while let p = phonemes.drain(..min(pack.as_ref().len(), phonemes.len()))
&& p.len() != 0
{
let phonemes = Array::from_shape_vec((1, p.len()), p.collect())?;
let ref_s = pack.as_ref()[phonemes.len() - 1]
.first()
.cloned()
.unwrap_or(vec![0.; 256]);
let style = Array::from_shape_vec((1, ref_s.len()), ref_s)?;
let speed = Array::from_vec(vec![speed]);
let options = RunOptions::new()?;
let mut model = model.lock().await;
let t = SystemTime::now();
let kokoro_output = model
.run_async(
inputs![
"input_ids" => TensorRef::from_array_view(&phonemes)?,
"style" => TensorRef::from_array_view(&style)?,
"speed" => TensorRef::from_array_view(&speed)?,
],
&options,
)?
.await?;
elapsed = t.elapsed()?;
let (_, audio) = kokoro_output["waveform"].try_extract_tensor::<f32>()?;
let (_, _duration) = kokoro_output["duration"].try_extract_tensor::<i64>()?;
// let _ = dbg!(duration.len());
ret.extend_from_slice(audio);
}
Ok((ret, elapsed))
}
pub(super) async fn synth<P, S>(
model: Weak<Mutex<Session>>,
text: S,
pack: P,
voice: Voice,
) -> Result<(Vec<f32>, Duration), KokoroError>
where
P: AsRef<Vec<Vec<Vec<f32>>>>,
S: AsRef<str>,
{
let phonemes = g2p(text.as_ref(), voice.is_v11_supported())?;
// #[cfg(debug_assertions)]
// println!("{}", phonemes);
match voice {
v if v.is_v11_supported() => synth_v11(model, phonemes, pack, v.get_speed_v11()?).await,
v if v.is_v10_supported() => synth_v10(model, phonemes, pack, v.get_speed_v10()?).await,
v => Err(KokoroError::VoiceVersionInvalid(v.get_name().to_owned())),
}
}
+324
View File
@@ -0,0 +1,324 @@
use {
log::warn,
std::{collections::HashMap, sync::LazyLock},
};
static VOCAB_V10: LazyLock<HashMap<char, u8>> = LazyLock::new(|| {
let mut map = HashMap::new();
map.insert(';', 1);
map.insert(':', 2);
map.insert(',', 3);
map.insert('.', 4);
map.insert('!', 5);
map.insert('?', 6);
map.insert('—', 9);
map.insert('…', 10);
map.insert('"', 11);
map.insert('(', 12);
map.insert(')', 13);
map.insert('“', 14);
map.insert('”', 15);
map.insert(' ', 16);
map.insert('\u{0303}', 17); // Unicode escape for combining tilde
map.insert('ʣ', 18);
map.insert('ʥ', 19);
map.insert('ʦ', 20);
map.insert('ʨ', 21);
map.insert('ᵝ', 22);
map.insert('\u{AB67}', 23); // Unicode escape
map.insert('A', 24);
map.insert('I', 25);
map.insert('O', 31);
map.insert('Q', 33);
map.insert('S', 35);
map.insert('T', 36);
map.insert('W', 39);
map.insert('Y', 41);
map.insert('ᵊ', 42);
map.insert('a', 43);
map.insert('b', 44);
map.insert('c', 45);
map.insert('d', 46);
map.insert('e', 47);
map.insert('f', 48);
map.insert('h', 50);
map.insert('i', 51);
map.insert('j', 52);
map.insert('k', 53);
map.insert('l', 54);
map.insert('m', 55);
map.insert('n', 56);
map.insert('o', 57);
map.insert('p', 58);
map.insert('q', 59);
map.insert('r', 60);
map.insert('s', 61);
map.insert('t', 62);
map.insert('u', 63);
map.insert('v', 64);
map.insert('w', 65);
map.insert('x', 66);
map.insert('y', 67);
map.insert('z', 68);
map.insert('ɑ', 69);
map.insert('ɐ', 70);
map.insert('ɒ', 71);
map.insert('æ', 72);
map.insert('β', 75);
map.insert('ɔ', 76);
map.insert('ɕ', 77);
map.insert('ç', 78);
map.insert('ɖ', 80);
map.insert('ð', 81);
map.insert('ʤ', 82);
map.insert('ə', 83);
map.insert('ɚ', 85);
map.insert('ɛ', 86);
map.insert('ɜ', 87);
map.insert('ɟ', 90);
map.insert('ɡ', 92);
map.insert('ɥ', 99);
map.insert('ɨ', 101);
map.insert('ɪ', 102);
map.insert('ʝ', 103);
map.insert('ɯ', 110);
map.insert('ɰ', 111);
map.insert('ŋ', 112);
map.insert('ɳ', 113);
map.insert('ɲ', 114);
map.insert('ɴ', 115);
map.insert('ø', 116);
map.insert('ɸ', 118);
map.insert('θ', 119);
map.insert('œ', 120);
map.insert('ɹ', 123);
map.insert('ɾ', 125);
map.insert('ɻ', 126);
map.insert('ʁ', 128);
map.insert('ɽ', 129);
map.insert('ʂ', 130);
map.insert('ʃ', 131);
map.insert('ʈ', 132);
map.insert('ʧ', 133);
map.insert('ʊ', 135);
map.insert('ʋ', 136);
map.insert('ʌ', 138);
map.insert('ɣ', 139);
map.insert('ɤ', 140);
map.insert('χ', 142);
map.insert('ʎ', 143);
map.insert('ʒ', 147);
map.insert('ʔ', 148);
map.insert('ˈ', 156);
map.insert('ˌ', 157);
map.insert('ː', 158);
map.insert('ʰ', 162);
map.insert('ʲ', 164);
map.insert('↓', 169);
map.insert('→', 171);
map.insert('↗', 172);
map.insert('↘', 173);
map.insert('ᵻ', 177);
map
});
static VOCAB_V11: LazyLock<HashMap<char, u8>> = LazyLock::new(|| {
let mut map = HashMap::new();
map.insert(';', 1);
map.insert(':', 2);
map.insert(',', 3);
map.insert('.', 4);
map.insert('!', 5);
map.insert('?', 6);
map.insert('/', 7);
map.insert('—', 9);
map.insert('…', 10);
map.insert('"', 11);
map.insert('(', 12);
map.insert(')', 13);
map.insert('“', 14);
map.insert('”', 15);
map.insert(' ', 16);
map.insert('\u{0303}', 17); // Unicode escape for combining tilde
map.insert('ʣ', 18);
map.insert('ʥ', 19);
map.insert('ʦ', 20);
map.insert('ʨ', 21);
map.insert('ᵝ', 22);
map.insert('ㄓ', 23);
map.insert('A', 24);
map.insert('I', 25);
map.insert('ㄅ', 30);
map.insert('O', 31);
map.insert('ㄆ', 32);
map.insert('Q', 33);
map.insert('R', 34);
map.insert('S', 35);
map.insert('T', 36);
map.insert('ㄇ', 37);
map.insert('ㄈ', 38);
map.insert('W', 39);
map.insert('ㄉ', 40);
map.insert('Y', 41);
map.insert('ᵊ', 42);
map.insert('a', 43);
map.insert('b', 44);
map.insert('c', 45);
map.insert('d', 46);
map.insert('e', 47);
map.insert('f', 48);
map.insert('ㄊ', 49);
map.insert('h', 50);
map.insert('i', 51);
map.insert('j', 52);
map.insert('k', 53);
map.insert('l', 54);
map.insert('m', 55);
map.insert('n', 56);
map.insert('o', 57);
map.insert('p', 58);
map.insert('q', 59);
map.insert('r', 60);
map.insert('s', 61);
map.insert('t', 62);
map.insert('u', 63);
map.insert('v', 64);
map.insert('w', 65);
map.insert('x', 66);
map.insert('y', 67);
map.insert('z', 68);
map.insert('ɑ', 69);
map.insert('ɐ', 70);
map.insert('ɒ', 71);
map.insert('æ', 72);
map.insert('ㄋ', 73);
map.insert('ㄌ', 74);
map.insert('β', 75);
map.insert('ɔ', 76);
map.insert('ɕ', 77);
map.insert('ç', 78);
map.insert('ㄍ', 79);
map.insert('ɖ', 80);
map.insert('ð', 81);
map.insert('ʤ', 82);
map.insert('ə', 83);
map.insert('ㄎ', 84);
map.insert('ㄦ', 85);
map.insert('ɛ', 86);
map.insert('ɜ', 87);
map.insert('ㄏ', 88);
map.insert('ㄐ', 89);
map.insert('ɟ', 90);
map.insert('ㄑ', 91);
map.insert('ɡ', 92);
map.insert('ㄒ', 93);
map.insert('ㄔ', 94);
map.insert('ㄕ', 95);
map.insert('ㄗ', 96);
map.insert('ㄘ', 97);
map.insert('ㄙ', 98);
map.insert('月', 99);
map.insert('ㄚ', 100);
map.insert('ɨ', 101);
map.insert('ɪ', 102);
map.insert('ʝ', 103);
map.insert('ㄛ', 104);
map.insert('ㄝ', 105);
map.insert('ㄞ', 106);
map.insert('ㄟ', 107);
map.insert('ㄠ', 108);
map.insert('ㄡ', 109);
map.insert('ɯ', 110);
map.insert('ɰ', 111);
map.insert('ŋ', 112);
map.insert('ɳ', 113);
map.insert('ɲ', 114);
map.insert('ɴ', 115);
map.insert('ø', 116);
map.insert('ㄢ', 117);
map.insert('ɸ', 118);
map.insert('θ', 119);
map.insert('œ', 120);
map.insert('ㄣ', 121);
map.insert('ㄤ', 122);
map.insert('ɹ', 123);
map.insert('ㄥ', 124);
map.insert('ɾ', 125);
map.insert('ㄖ', 126);
map.insert('ㄧ', 127);
map.insert('ʁ', 128);
map.insert('ɽ', 129);
map.insert('ʂ', 130);
map.insert('ʃ', 131);
map.insert('ʈ', 132);
map.insert('ʧ', 133);
map.insert('ㄨ', 134);
map.insert('ʊ', 135);
map.insert('ʋ', 136);
map.insert('ㄩ', 137);
map.insert('ʌ', 138);
map.insert('ɣ', 139);
map.insert('ㄜ', 140);
map.insert('ㄭ', 141);
map.insert('χ', 142);
map.insert('ʎ', 143);
map.insert('十', 144);
map.insert('压', 145);
map.insert('言', 146);
map.insert('ʒ', 147);
map.insert('ʔ', 148);
map.insert('阳', 149);
map.insert('要', 150);
map.insert('阴', 151);
map.insert('应', 152);
map.insert('用', 153);
map.insert('又', 154);
map.insert('中', 155);
map.insert('ˈ', 156);
map.insert('ˌ', 157);
map.insert('ː', 158);
map.insert('穵', 159);
map.insert('外', 160);
map.insert('万', 161);
map.insert('ʰ', 162);
map.insert('王', 163);
map.insert('ʲ', 164);
map.insert('为', 165);
map.insert('文', 166);
map.insert('瓮', 167);
map.insert('我', 168);
map.insert('3', 169);
map.insert('5', 170);
map.insert('1', 171);
map.insert('2', 172);
map.insert('4', 173);
map.insert('元', 175);
map.insert('云', 176);
map.insert('ᵻ', 177);
map
});
pub fn get_token_ids(phonemes: &str, v11: bool) -> Vec<i64> {
let mut tokens = Vec::with_capacity(phonemes.len() + 2);
tokens.push(0);
for i in phonemes.chars() {
let v = if v11 {
VOCAB_V11.get(&i).copied()
} else {
VOCAB_V10.get(&i).copied()
};
match v {
Some(t) => {
tokens.push(t as _);
}
_ => {
warn!("Unknown phone {}, skipped.", i);
}
}
}
tokens.push(0);
tokens
}
+4
View File
@@ -0,0 +1,4 @@
mod en;
mod zh;
pub use {en::*, zh::*};
+147
View File
@@ -0,0 +1,147 @@
use regex::Regex;
use std::{collections::HashMap, sync::LazyLock};
static LETTERS_IPA_MAP: LazyLock<HashMap<char, &'static str>> = LazyLock::new(|| {
let mut map = HashMap::new();
map.insert('a', "ɐ");
map.insert('b', "bˈi");
map.insert('c', "sˈi");
map.insert('d', "dˈi");
map.insert('e', "ˈi");
map.insert('f', "ˈɛf");
map.insert('g', "ʤˈi");
map.insert('h', "ˈ");
map.insert('i', "ˈI");
map.insert('j', "ʤˈA");
map.insert('k', "kˈA");
map.insert('l', "ˈɛl");
map.insert('m', "ˈɛm");
map.insert('n', "ˈɛn");
map.insert('o', "ˈO");
map.insert('p', "pˈi");
map.insert('q', "kjˈu");
map.insert('r', "ˈɑɹ");
map.insert('s', "ˈɛs");
map.insert('t', "tˈi");
map.insert('u', "jˈu");
map.insert('v', "vˈi");
map.insert('w', "dˈʌbᵊlju");
map.insert('x', "ˈɛks");
map.insert('y', "wˈI");
map.insert('z', "zˈi");
map.insert('A', "ˈA");
map.insert('B', "bˈi");
map.insert('C', "sˈi");
map.insert('D', "dˈi");
map.insert('E', "ˈi");
map.insert('F', "ˈɛf");
map.insert('G', "ʤˈi");
map.insert('H', "ˈ");
map.insert('I', "ˈI");
map.insert('J', "ʤˈA");
map.insert('K', "kˈA");
map.insert('L', "ˈɛl");
map.insert('M', "ˈɛm");
map.insert('N', "ˈɛn");
map.insert('O', "ˈO");
map.insert('P', "pˈi");
map.insert('Q', "kjˈu");
map.insert('R', "ˈɑɹ");
map.insert('S', "ˈɛs");
map.insert('T', "tˈi");
map.insert('U', "jˈu");
map.insert('V', "vˈi");
map.insert('W', "dˈʌbᵊlju");
map.insert('X', "ˈɛks");
map.insert('Y', "wˈI");
map.insert('Z', "zˈi");
map
});
static ARPA_IPA_MAP: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
let mut map = HashMap::new();
map.insert("AA", "ɑ");
map.insert("AE", "æ");
map.insert("AH", "ə");
map.insert("AO", "ɔ");
map.insert("AW", "");
map.insert("AY", "aɪ");
map.insert("B", "b");
map.insert("CH", "");
map.insert("D", "d");
map.insert("DH", "ð");
map.insert("EH", "ɛ");
map.insert("ER", "ɝ");
map.insert("EY", "eɪ");
map.insert("F", "f");
map.insert("G", "ɡ");
map.insert("HH", "h");
map.insert("IH", "ɪ");
map.insert("IY", "i");
map.insert("JH", "");
map.insert("K", "k");
map.insert("L", "l");
map.insert("M", "m");
map.insert("N", "n");
map.insert("NG", "ŋ");
map.insert("OW", "");
map.insert("OY", "ɔɪ");
map.insert("P", "p");
map.insert("R", "ɹ");
map.insert("S", "s");
map.insert("SH", "ʃ");
map.insert("T", "t");
map.insert("TH", "θ");
map.insert("UH", "ʊ");
map.insert("UW", "u");
map.insert("V", "v");
map.insert("W", "w");
map.insert("Y", "j");
map.insert("Z", "z");
map.insert("ZH", "ʒ");
map.insert("SIL", "");
map
});
/// 支持2025新增符号(如:吸气音ʘ)
const SPECIAL_CASES: [(&str, &str); 3] = [("CLICK!", "ʘ"), ("TSK!", "ǀ"), ("TUT!", "ǁ")];
pub fn arpa_to_ipa(arpa: &str) -> Result<String, regex::Error> {
let re = Regex::new(r"([A-Z!]+)(\d*)")?;
let Some(caps) = re.captures(arpa) else {
return Ok(Default::default());
};
// 处理特殊符号(2025新增)
if let Some(sc) = SPECIAL_CASES.iter().find(|&&(s, _)| s == &caps[1]) {
return Ok(sc.1.to_string());
}
// 获取IPA映射
let phoneme = ARPA_IPA_MAP
.get(&caps[1])
.map_or_else(|| letters_to_ipa(arpa), |i| i.to_string());
let mut result = String::with_capacity(arpa.len() * 2);
// 添加重音标记(支持三级重音)
result.push(match &caps[2] {
"1" => 'ˈ',
"2" => 'ˌ',
"3" => '˧', // 2025新增中级重音
_ => '\0',
});
result.push_str(&phoneme);
Ok(result)
}
pub fn letters_to_ipa(letters: &str) -> String {
let mut res = String::with_capacity(letters.len());
for i in letters.chars() {
if let Some(p) = LETTERS_IPA_MAP.get(&i) {
res.push_str(p);
}
}
res
}
File diff suppressed because it is too large Load Diff
+364
View File
@@ -0,0 +1,364 @@
/// 汉语拼音到国际音标的转换
/// 参考了python的misaki库的zh.py。
use std::{collections::HashMap, error::Error, fmt, sync::LazyLock};
const VALID_FINALS: [&str; 37] = [
"i", "u", "ü", "a", "ia", "ua", "o", "uo", "e", "ie", "üe", "ai", "uai", "ei", "uei", "ao",
"iao", "ou", "iou", "an", "ian", "uan", "üan", "en", "in", "uen", "ün", "ang", "iang", "uang",
"eng", "ing", "ueng", "ong", "iong", "er", "ê",
];
const INITIALS: [&str; 21] = [
"zh", "ch", "sh", "b", "c", "d", "f", "g", "h", "j", "k", "l", "m", "n", "p", "q", "r", "s",
"t", "x", "z",
];
// 错误类型定义
#[derive(Debug)]
pub enum PinyinError {
FinalNotFound(String),
}
impl fmt::Display for PinyinError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PinyinError::FinalNotFound(tip) => write!(f, "Final not found: {}", tip),
}
}
}
impl Error for PinyinError {}
static INITIAL_MAPPING: LazyLock<HashMap<&'static str, Vec<Vec<&'static str>>>> =
LazyLock::new(|| {
let mut map = HashMap::new();
map.insert("b", vec![vec!["p"]]);
map.insert("c", vec![vec!["ʦʰ"]]);
map.insert("ch", vec![vec!["ꭧʰ"]]);
map.insert("d", vec![vec!["t"]]);
map.insert("f", vec![vec!["f"]]);
map.insert("g", vec![vec!["k"]]);
map.insert("h", vec![vec!["x"], vec!["h"]]);
map.insert("j", vec![vec!["ʨ"]]);
map.insert("k", vec![vec![""]]);
map.insert("l", vec![vec!["l"]]);
map.insert("m", vec![vec!["m"]]);
map.insert("n", vec![vec!["n"]]);
map.insert("p", vec![vec![""]]);
map.insert("q", vec![vec!["ʨʰ"]]);
map.insert("r", vec![vec!["ɻ"], vec!["ʐ"]]);
map.insert("s", vec![vec!["s"]]);
map.insert("sh", vec![vec!["ʂ"]]);
map.insert("t", vec![vec![""]]);
map.insert("x", vec![vec!["ɕ"]]);
map.insert("z", vec![vec!["ʦ"]]);
map.insert("zh", vec![vec![""]]);
map
});
static SYLLABIC_CONSONANT_MAPPINGS: LazyLock<HashMap<&'static str, Vec<Vec<&'static str>>>> =
LazyLock::new(|| {
let mut map = HashMap::new();
map.insert("hm", vec![vec!["h", "m0"]]);
map.insert("hng", vec![vec!["h", "ŋ0"]]);
map.insert("m", vec![vec!["m0"]]);
map.insert("n", vec![vec!["n0"]]);
map.insert("ng", vec![vec!["ŋ0"]]);
map
});
static INTERJECTION_MAPPINGS: LazyLock<HashMap<&'static str, Vec<Vec<&'static str>>>> =
LazyLock::new(|| {
let mut map = HashMap::new();
map.insert("io", vec![vec!["j", "ɔ0"]]);
map.insert("ê", vec![vec!["ɛ0"]]);
map.insert("er", vec![vec!["ɚ0"], vec!["aɚ̯0"]]);
map.insert("o", vec![vec!["ɔ0"]]);
map
});
/// Duanmu (2000, p. 37) and Lin (2007, p. 68f)
/// Diphtongs from Duanmu (2007, p. 40): au, əu, əi, ai
/// Diphthongs from Lin (2007, p. 68f): au̯, ou̯, ei̯, ai̯
static FINAL_MAPPING: LazyLock<HashMap<&'static str, Vec<Vec<&'static str>>>> =
LazyLock::new(|| {
let mut map = HashMap::new();
map.insert("a", vec![vec!["a0"]]);
map.insert("ai", vec![vec!["ai0"]]);
map.insert("an", vec![vec!["a0", "n"]]);
map.insert("ang", vec![vec!["a0", "ŋ"]]);
map.insert("ao", vec![vec!["au0"]]);
map.insert("e", vec![vec!["ɤ0"]]);
map.insert("ei", vec![vec!["ei0"]]);
map.insert("en", vec![vec!["ə0", "n"]]);
map.insert("eng", vec![vec!["ə0", "ŋ"]]);
map.insert("i", vec![vec!["i0"]]);
map.insert("ia", vec![vec!["j", "a0"]]);
map.insert("ian", vec![vec!["j", "ɛ0", "n"]]);
map.insert("iang", vec![vec!["j", "a0", "ŋ"]]);
map.insert("iao", vec![vec!["j", "au0"]]);
map.insert("ie", vec![vec!["j", "e0"]]);
map.insert("in", vec![vec!["i0", "n"]]);
map.insert("iou", vec![vec!["j", "ou0"]]);
map.insert("ing", vec![vec!["i0", "ŋ"]]);
map.insert("iong", vec![vec!["j", "ʊ0", "ŋ"]]);
map.insert("ong", vec![vec!["ʊ0", "ŋ"]]);
map.insert("ou", vec![vec!["ou0"]]);
map.insert("u", vec![vec!["u0"]]);
map.insert("uei", vec![vec!["w", "ei0"]]);
map.insert("ua", vec![vec!["w", "a0"]]);
map.insert("uai", vec![vec!["w", "ai0"]]);
map.insert("uan", vec![vec!["w", "a0", "n"]]);
map.insert("uen", vec![vec!["w", "ə0", "n"]]);
map.insert("uang", vec![vec!["w", "a0", "ŋ"]]);
map.insert("ueng", vec![vec!["w", "ə0", "ŋ"]]);
map.insert("ui", vec![vec!["w", "ei0"]]);
map.insert("un", vec![vec!["w", "ə0", "n"]]);
map.insert("uo", vec![vec!["w", "o0"]]);
map.insert("o", vec![vec!["w", "o0"]]); // 注意:这里'o'的映射可能与预期不符,根据注释可能需要特殊处理
map.insert("ü", vec![vec!["y0"]]);
map.insert("üe", vec![vec!["ɥ", "e0"]]);
map.insert("üan", vec![vec!["ɥ", "ɛ0", "n"]]);
map.insert("ün", vec![vec!["y0", "n"]]);
map
});
static FINAL_MAPPING_AFTER_ZH_CH_SH_R: LazyLock<HashMap<&'static str, Vec<Vec<&'static str>>>> =
LazyLock::new(|| {
let mut map = HashMap::new();
map.insert("i", vec![vec!["ɻ0"], vec!["ʐ0"]]);
map
});
static FINAL_MAPPING_AFTER_Z_C_S: LazyLock<HashMap<&'static str, Vec<Vec<&'static str>>>> =
LazyLock::new(|| {
let mut map = HashMap::new();
map.insert("i", vec![vec!["ɹ0"], vec!["z0"]]);
map
});
static TONE_MAPPING: LazyLock<HashMap<u8, &'static str>> = LazyLock::new(|| {
let mut map = HashMap::new();
map.insert(1u8, "˥");
map.insert(2u8, "˧˥");
map.insert(3u8, "˧˩˧");
map.insert(4u8, "˥˩");
map.insert(5u8, "");
map
});
pub(crate) fn split_tone(pinyin: &str) -> (&str, u8) {
if let Some(t) = pinyin
.chars()
.last()
.and_then(|c| c.to_digit(10).map(|n| n as u8))
{
return (&pinyin[..pinyin.len() - 1], t);
}
(pinyin, 5)
}
/// uen 转换,还原原始的韵母
/// iou,ueiuen前面加声母的时候,写成iu,ui,un。
/// 例如niu(牛)gui(归)lun(论)。
fn convert_uen(s: &str) -> String {
match s.strip_suffix('n') {
Some(stem) if stem.ends_with(['u', 'ū', 'ú', 'ǔ', 'ù']) => {
format!("{}en", stem)
}
_ => s.to_string(),
}
}
/// ü 转换,还原原始的韵母
/// ü行的韵母跟声母j,q,x拼的时候,写成ju(居),qu(区),xu(虚), ü上两点也省略;
/// 但是跟声母n,l拼的时候,仍然写成nü(女),lü(吕)
fn convert_uv(pinyin: &str) -> String {
let chars = pinyin.chars().collect::<Vec<_>>();
match chars.as_slice() {
[
c @ ('j' | 'q' | 'x'),
tone @ ('u' | 'ū' | 'ú' | 'ǔ' | 'ù'),
rest @ ..,
] => {
let new_tone = match tone {
'u' => 'ü',
'ū' => 'ǖ',
'ú' => 'ǘ',
'ǔ' => 'ǚ',
'ù' => 'ǜ',
_ => unreachable!(),
};
format!("{}{}{}", c, new_tone, rest.iter().collect::<String>())
}
_ => pinyin.to_string(),
}
}
/// iou 转换,还原原始的韵母
/// iou,ueiuen前面加声母的时候,写成iu,ui,un。
/// 例如niu(牛)gui(归)lun(论)。
fn convert_iou(pinyin: &str) -> String {
let chars = pinyin.chars().collect::<Vec<_>>();
match chars.as_slice() {
// 处理 iu 系列
[.., 'i', u @ ('u' | 'ū' | 'ú' | 'ǔ' | 'ù')] => {
format!("{}o{}", &pinyin[..pinyin.len() - 1], u)
}
// 其他情况保持原样
_ => pinyin.to_string(),
}
}
/// uei 转换,还原原始的韵母
/// iou,ueiuen前面加声母的时候,写成iu,ui,un。
/// 例如niu(牛)gui(归)lun(论)。
fn convert_uei(pinyin: &str) -> String {
let chars = pinyin.chars().collect::<Vec<_>>();
match chars.as_slice() {
// 处理 ui 系列
[.., 'u', i @ ('i' | 'ī' | 'í' | 'ǐ' | 'ì')] => {
format!("{}e{}", &pinyin[..pinyin.len() - 1], i)
}
// 其他情况保持原样
_ => pinyin.to_string(),
}
}
/// 零声母转换,还原原始的韵母
/// i行的韵母,前面没有声母的时候,写成yi(衣),ya(呀)ye(耶)yao(腰)you(忧)yan(烟)yin(因)yang(央)ying(英)yong(雍)。
/// u行的韵母,前面没有声母的时候,写成wu(乌),wa(蛙)wo(窝)wai(歪)wei(威)wan(弯)wen(温)wang(汪)weng(翁)。
/// ü行的韵母,前面没有声母的时候,写成yu(迂),yue(约),yuan(冤)yun(晕);ü上两点省略。"""
pub(crate) fn convert_zero_consonant(pinyin: &str) -> String {
let mut buffer = String::with_capacity(pinyin.len() + 2);
let chars: Vec<char> = pinyin.chars().collect();
match chars.as_slice() {
// 处理Y系转换
['y', 'u', rest @ ..] => {
buffer.push('ü');
buffer.extend(rest.iter());
}
['y', u @ ('ū' | 'ú' | 'ǔ' | 'ù'), rest @ ..] => {
buffer.push(match u {
'ū' => 'ǖ', // ü 第一声
'ú' => 'ǘ', // ü 第二声
'ǔ' => 'ǚ', // ü 第三声
'ù' => 'ǜ', // ü 第四声
_ => unreachable!(),
});
buffer.extend(rest.iter());
}
['y', i @ ('i' | 'ī' | 'í' | 'ǐ' | 'ì'), rest @ ..] => {
buffer.push(*i);
buffer.extend(rest.iter());
}
['y', rest @ ..] => {
buffer.push('i');
buffer.extend(rest);
}
// 处理W系转换
['w', u @ ('u' | 'ū' | 'ú' | 'ǔ' | 'ù'), rest @ ..] => {
buffer.push(*u);
buffer.extend(rest.iter());
}
['w', rest @ ..] => {
buffer.push('u');
buffer.extend(rest);
}
// 无需转换的情况
_ => return pinyin.to_string(),
}
// 有效性验证
if VALID_FINALS.contains(&buffer.as_str()) {
buffer
} else {
pinyin.to_string()
}
}
pub(crate) fn split_initial(pinyin: &str) -> (&'static str, &str) {
for &initial in &INITIALS {
if let Some(stripped) = pinyin.strip_prefix(initial) {
return (initial, stripped);
}
}
("", pinyin)
}
fn apply_tone(variants: &[Vec<&str>], tone: u8) -> Vec<Vec<String>> {
let tone_str = TONE_MAPPING.get(&tone).unwrap_or(&"");
variants
.iter()
.map(|v| v.iter().map(|s| s.replace("0", tone_str)).collect())
.collect()
}
pub fn pinyin_to_ipa(pinyin: &str) -> Result<Vec<Vec<String>>, PinyinError> {
let (pinyin, tone) = split_tone(pinyin);
let pinyin = convert_zero_consonant(pinyin);
let pinyin = convert_uv(&pinyin);
let pinyin = convert_iou(&pinyin);
let pinyin = convert_uei(&pinyin);
let pinyin = convert_uen(&pinyin);
// 处理特殊成音节辅音和感叹词
if let Some(ipa) = SYLLABIC_CONSONANT_MAPPINGS.get(pinyin.as_str()) {
return Ok(apply_tone(ipa, tone)
.into_iter()
.map(|i| i.into_iter().collect())
.collect());
}
if let Some(ipa) = INTERJECTION_MAPPINGS.get(pinyin.as_str()) {
return Ok(apply_tone(ipa, tone)
.into_iter()
.map(|i| i.into_iter().collect())
.collect());
}
// 分解声母韵母
let (initial_part, final_part) = split_initial(pinyin.as_str());
// 获取韵母IPA
let final_ipa = match initial_part {
"zh" | "ch" | "sh" | "r" if FINAL_MAPPING_AFTER_ZH_CH_SH_R.contains_key(final_part) => {
FINAL_MAPPING_AFTER_ZH_CH_SH_R.get(final_part)
}
"z" | "c" | "s" if FINAL_MAPPING_AFTER_Z_C_S.contains_key(final_part) => {
FINAL_MAPPING_AFTER_Z_C_S.get(final_part)
}
_ => FINAL_MAPPING.get(final_part),
}
.ok_or(PinyinError::FinalNotFound(final_part.to_owned()))?;
// 组合所有可能
let mut result = Vec::<Vec<String>>::new();
let initials = INITIAL_MAPPING
.get(initial_part)
.map_or(vec![vec![Default::default()]], |i| {
i.iter()
.map(|i| i.iter().map(|i| i.to_string()).collect())
.collect()
});
for i in initials.into_iter() {
for j in apply_tone(final_ipa, tone).into_iter() {
result.push(
i.iter()
.chain(j.iter())
.map(|i| i.to_owned())
.collect::<Vec<_>>(),
)
}
}
Ok(result)
}
+673
View File
@@ -0,0 +1,673 @@
use crate::KokoroError;
//noinspection SpellCheckingInspection
#[derive(Copy, Clone, Debug)]
pub enum Voice {
// v1.0
ZmYunyang(f32),
ZfXiaoni(f32),
AfJessica(f32),
BfLily(f32),
ZfXiaobei(f32),
ZmYunxia(f32),
AfHeart(f32),
BfEmma(f32),
AmPuck(f32),
BfAlice(f32),
HfAlpha(f32),
BfIsabella(f32),
AfNova(f32),
AmFenrir(f32),
EmAlex(f32),
ImNicola(f32),
PmAlex(f32),
AfAlloy(f32),
ZmYunxi(f32),
AfSarah(f32),
JfNezumi(f32),
BmDaniel(f32),
JfTebukuro(f32),
JfAlpha(f32),
JmKumo(f32),
EmSanta(f32),
AmLiam(f32),
AmSanta(f32),
AmEric(f32),
BmFable(f32),
AfBella(f32),
BmLewis(f32),
PfDora(f32),
AfNicole(f32),
BmGeorge(f32),
AmOnyx(f32),
HmPsi(f32),
HfBeta(f32),
HmOmega(f32),
ZfXiaoxiao(f32),
FfSiwis(f32),
EfDora(f32),
AfAoede(f32),
AmEcho(f32),
AmMichael(f32),
AfKore(f32),
ZfXiaoyi(f32),
JfGongitsune(f32),
AmAdam(f32),
IfSara(f32),
AfSky(f32),
PmSanta(f32),
AfRiver(f32),
ZmYunjian(f32),
// v1.1
Zm029(i32),
Zf048(i32),
Zf008(i32),
Zm014(i32),
Zf003(i32),
Zf047(i32),
Zm080(i32),
Zf094(i32),
Zf046(i32),
Zm054(i32),
Zf001(i32),
Zm062(i32),
BfVale(i32),
Zf044(i32),
Zf005(i32),
Zf028(i32),
Zf059(i32),
Zm030(i32),
Zf074(i32),
Zm009(i32),
Zf004(i32),
Zf021(i32),
Zm095(i32),
Zm041(i32),
Zf087(i32),
Zf039(i32),
Zm031(i32),
Zf007(i32),
Zf038(i32),
Zf092(i32),
Zm056(i32),
Zf099(i32),
Zm010(i32),
Zm069(i32),
Zm016(i32),
Zm068(i32),
Zf083(i32),
Zf093(i32),
Zf006(i32),
Zf026(i32),
Zm053(i32),
Zm064(i32),
AfSol(i32),
Zf042(i32),
Zf084(i32),
Zf073(i32),
Zf067(i32),
Zm025(i32),
Zm020(i32),
Zm050(i32),
Zf070(i32),
Zf002(i32),
Zf032(i32),
Zm091(i32),
Zm066(i32),
Zm089(i32),
Zm034(i32),
Zm100(i32),
Zf086(i32),
Zf040(i32),
Zm011(i32),
Zm098(i32),
Zm015(i32),
Zf051(i32),
Zm065(i32),
Zf076(i32),
Zf036(i32),
Zm033(i32),
Zf018(i32),
Zf017(i32),
Zf049(i32),
AfMaple(i32),
Zm082(i32),
Zm057(i32),
Zf079(i32),
Zf022(i32),
Zm063(i32),
Zf060(i32),
Zf019(i32),
Zm097(i32),
Zm096(i32),
Zf023(i32),
Zf027(i32),
Zf085(i32),
Zf077(i32),
Zm035(i32),
Zf088(i32),
Zf024(i32),
Zf072(i32),
Zm055(i32),
Zm052(i32),
Zf071(i32),
Zm061(i32),
Zf078(i32),
Zm013(i32),
Zm081(i32),
Zm037(i32),
Zf090(i32),
Zf043(i32),
Zm058(i32),
Zm012(i32),
Zm045(i32),
Zf075(i32),
}
impl Voice {
//noinspection SpellCheckingInspection
pub(super) fn get_name(&self) -> &str {
match self {
Self::ZmYunyang(_) => "zm_yunyang",
Self::ZfXiaoni(_) => "zf_xiaoni",
Self::AfJessica(_) => "af_jessica",
Self::BfLily(_) => "bf_lily",
Self::ZfXiaobei(_) => "zf_xiaobei",
Self::ZmYunxia(_) => "zm_yunxia",
Self::AfHeart(_) => "af_heart",
Self::BfEmma(_) => "bf_emma",
Self::AmPuck(_) => "am_puck",
Self::BfAlice(_) => "bf_alice",
Self::HfAlpha(_) => "hf_alpha",
Self::BfIsabella(_) => "bf_isabella",
Self::AfNova(_) => "af_nova",
Self::AmFenrir(_) => "am_fenrir",
Self::EmAlex(_) => "em_alex",
Self::ImNicola(_) => "im_nicola",
Self::PmAlex(_) => "pm_alex",
Self::AfAlloy(_) => "af_alloy",
Self::ZmYunxi(_) => "zm_yunxi",
Self::AfSarah(_) => "af_sarah",
Self::JfNezumi(_) => "jf_nezumi",
Self::BmDaniel(_) => "bm_daniel",
Self::JfTebukuro(_) => "jf_tebukuro",
Self::JfAlpha(_) => "jf_alpha",
Self::JmKumo(_) => "jm_kumo",
Self::EmSanta(_) => "em_santa",
Self::AmLiam(_) => "am_liam",
Self::AmSanta(_) => "am_santa",
Self::AmEric(_) => "am_eric",
Self::BmFable(_) => "bm_fable",
Self::AfBella(_) => "af_bella",
Self::BmLewis(_) => "bm_lewis",
Self::PfDora(_) => "pf_dora",
Self::AfNicole(_) => "af_nicole",
Self::BmGeorge(_) => "bm_george",
Self::AmOnyx(_) => "am_onyx",
Self::HmPsi(_) => "hm_psi",
Self::HfBeta(_) => "hf_beta",
Self::HmOmega(_) => "hm_omega",
Self::ZfXiaoxiao(_) => "zf_xiaoxiao",
Self::FfSiwis(_) => "ff_siwis",
Self::EfDora(_) => "ef_dora",
Self::AfAoede(_) => "af_aoede",
Self::AmEcho(_) => "am_echo",
Self::AmMichael(_) => "am_michael",
Self::AfKore(_) => "af_kore",
Self::ZfXiaoyi(_) => "zf_xiaoyi",
Self::JfGongitsune(_) => "jf_gongitsune",
Self::AmAdam(_) => "am_adam",
Self::IfSara(_) => "if_sara",
Self::AfSky(_) => "af_sky",
Self::PmSanta(_) => "pm_santa",
Self::AfRiver(_) => "af_river",
Self::ZmYunjian(_) => "zm_yunjian",
Self::Zm029(_) => "zm_029",
Self::Zf048(_) => "zf_048",
Self::Zf008(_) => "zf_008",
Self::Zm014(_) => "zm_014",
Self::Zf003(_) => "zf_003",
Self::Zf047(_) => "zf_047",
Self::Zm080(_) => "zm_080",
Self::Zf094(_) => "zf_094",
Self::Zf046(_) => "zf_046",
Self::Zm054(_) => "zm_054",
Self::Zf001(_) => "zf_001",
Self::Zm062(_) => "zm_062",
Self::BfVale(_) => "bf_vale",
Self::Zf044(_) => "zf_044",
Self::Zf005(_) => "zf_005",
Self::Zf028(_) => "zf_028",
Self::Zf059(_) => "zf_059",
Self::Zm030(_) => "zm_030",
Self::Zf074(_) => "zf_074",
Self::Zm009(_) => "zm_009",
Self::Zf004(_) => "zf_004",
Self::Zf021(_) => "zf_021",
Self::Zm095(_) => "zm_095",
Self::Zm041(_) => "zm_041",
Self::Zf087(_) => "zf_087",
Self::Zf039(_) => "zf_039",
Self::Zm031(_) => "zm_031",
Self::Zf007(_) => "zf_007",
Self::Zf038(_) => "zf_038",
Self::Zf092(_) => "zf_092",
Self::Zm056(_) => "zm_056",
Self::Zf099(_) => "zf_099",
Self::Zm010(_) => "zm_010",
Self::Zm069(_) => "zm_069",
Self::Zm016(_) => "zm_016",
Self::Zm068(_) => "zm_068",
Self::Zf083(_) => "zf_083",
Self::Zf093(_) => "zf_093",
Self::Zf006(_) => "zf_006",
Self::Zf026(_) => "zf_026",
Self::Zm053(_) => "zm_053",
Self::Zm064(_) => "zm_064",
Self::AfSol(_) => "af_sol",
Self::Zf042(_) => "zf_042",
Self::Zf084(_) => "zf_084",
Self::Zf073(_) => "zf_073",
Self::Zf067(_) => "zf_067",
Self::Zm025(_) => "zm_025",
Self::Zm020(_) => "zm_020",
Self::Zm050(_) => "zm_050",
Self::Zf070(_) => "zf_070",
Self::Zf002(_) => "zf_002",
Self::Zf032(_) => "zf_032",
Self::Zm091(_) => "zm_091",
Self::Zm066(_) => "zm_066",
Self::Zm089(_) => "zm_089",
Self::Zm034(_) => "zm_034",
Self::Zm100(_) => "zm_100",
Self::Zf086(_) => "zf_086",
Self::Zf040(_) => "zf_040",
Self::Zm011(_) => "zm_011",
Self::Zm098(_) => "zm_098",
Self::Zm015(_) => "zm_015",
Self::Zf051(_) => "zf_051",
Self::Zm065(_) => "zm_065",
Self::Zf076(_) => "zf_076",
Self::Zf036(_) => "zf_036",
Self::Zm033(_) => "zm_033",
Self::Zf018(_) => "zf_018",
Self::Zf017(_) => "zf_017",
Self::Zf049(_) => "zf_049",
Self::AfMaple(_) => "af_maple",
Self::Zm082(_) => "zm_082",
Self::Zm057(_) => "zm_057",
Self::Zf079(_) => "zf_079",
Self::Zf022(_) => "zf_022",
Self::Zm063(_) => "zm_063",
Self::Zf060(_) => "zf_060",
Self::Zf019(_) => "zf_019",
Self::Zm097(_) => "zm_097",
Self::Zm096(_) => "zm_096",
Self::Zf023(_) => "zf_023",
Self::Zf027(_) => "zf_027",
Self::Zf085(_) => "zf_085",
Self::Zf077(_) => "zf_077",
Self::Zm035(_) => "zm_035",
Self::Zf088(_) => "zf_088",
Self::Zf024(_) => "zf_024",
Self::Zf072(_) => "zf_072",
Self::Zm055(_) => "zm_055",
Self::Zm052(_) => "zm_052",
Self::Zf071(_) => "zf_071",
Self::Zm061(_) => "zm_061",
Self::Zf078(_) => "zf_078",
Self::Zm013(_) => "zm_013",
Self::Zm081(_) => "zm_081",
Self::Zm037(_) => "zm_037",
Self::Zf090(_) => "zf_090",
Self::Zf043(_) => "zf_043",
Self::Zm058(_) => "zm_058",
Self::Zm012(_) => "zm_012",
Self::Zm045(_) => "zm_045",
Self::Zf075(_) => "zf_075",
}
}
pub(super) fn is_v10_supported(&self) -> bool {
matches!(
self,
Self::ZmYunyang(_)
| Self::ZfXiaoni(_)
| Self::AfJessica(_)
| Self::BfLily(_)
| Self::ZfXiaobei(_)
| Self::ZmYunxia(_)
| Self::AfHeart(_)
| Self::BfEmma(_)
| Self::AmPuck(_)
| Self::BfAlice(_)
| Self::HfAlpha(_)
| Self::BfIsabella(_)
| Self::AfNova(_)
| Self::AmFenrir(_)
| Self::EmAlex(_)
| Self::ImNicola(_)
| Self::PmAlex(_)
| Self::AfAlloy(_)
| Self::ZmYunxi(_)
| Self::AfSarah(_)
| Self::JfNezumi(_)
| Self::BmDaniel(_)
| Self::JfTebukuro(_)
| Self::JfAlpha(_)
| Self::JmKumo(_)
| Self::EmSanta(_)
| Self::AmLiam(_)
| Self::AmSanta(_)
| Self::AmEric(_)
| Self::BmFable(_)
| Self::AfBella(_)
| Self::BmLewis(_)
| Self::PfDora(_)
| Self::AfNicole(_)
| Self::BmGeorge(_)
| Self::AmOnyx(_)
| Self::HmPsi(_)
| Self::HfBeta(_)
| Self::HmOmega(_)
| Self::ZfXiaoxiao(_)
| Self::FfSiwis(_)
| Self::EfDora(_)
| Self::AfAoede(_)
| Self::AmEcho(_)
| Self::AmMichael(_)
| Self::AfKore(_)
| Self::ZfXiaoyi(_)
| Self::JfGongitsune(_)
| Self::AmAdam(_)
| Self::IfSara(_)
| Self::AfSky(_)
| Self::PmSanta(_)
| Self::AfRiver(_)
| Self::ZmYunjian(_)
)
}
pub(super) fn is_v11_supported(&self) -> bool {
matches!(
self,
Self::Zm029(_)
| Self::Zf048(_)
| Self::Zf008(_)
| Self::Zm014(_)
| Self::Zf003(_)
| Self::Zf047(_)
| Self::Zm080(_)
| Self::Zf094(_)
| Self::Zf046(_)
| Self::Zm054(_)
| Self::Zf001(_)
| Self::Zm062(_)
| Self::BfVale(_)
| Self::Zf044(_)
| Self::Zf005(_)
| Self::Zf028(_)
| Self::Zf059(_)
| Self::Zm030(_)
| Self::Zf074(_)
| Self::Zm009(_)
| Self::Zf004(_)
| Self::Zf021(_)
| Self::Zm095(_)
| Self::Zm041(_)
| Self::Zf087(_)
| Self::Zf039(_)
| Self::Zm031(_)
| Self::Zf007(_)
| Self::Zf038(_)
| Self::Zf092(_)
| Self::Zm056(_)
| Self::Zf099(_)
| Self::Zm010(_)
| Self::Zm069(_)
| Self::Zm016(_)
| Self::Zm068(_)
| Self::Zf083(_)
| Self::Zf093(_)
| Self::Zf006(_)
| Self::Zf026(_)
| Self::Zm053(_)
| Self::Zm064(_)
| Self::AfSol(_)
| Self::Zf042(_)
| Self::Zf084(_)
| Self::Zf073(_)
| Self::Zf067(_)
| Self::Zm025(_)
| Self::Zm020(_)
| Self::Zm050(_)
| Self::Zf070(_)
| Self::Zf002(_)
| Self::Zf032(_)
| Self::Zm091(_)
| Self::Zm066(_)
| Self::Zm089(_)
| Self::Zm034(_)
| Self::Zm100(_)
| Self::Zf086(_)
| Self::Zf040(_)
| Self::Zm011(_)
| Self::Zm098(_)
| Self::Zm015(_)
| Self::Zf051(_)
| Self::Zm065(_)
| Self::Zf076(_)
| Self::Zf036(_)
| Self::Zm033(_)
| Self::Zf018(_)
| Self::Zf017(_)
| Self::Zf049(_)
| Self::AfMaple(_)
| Self::Zm082(_)
| Self::Zm057(_)
| Self::Zf079(_)
| Self::Zf022(_)
| Self::Zm063(_)
| Self::Zf060(_)
| Self::Zf019(_)
| Self::Zm097(_)
| Self::Zm096(_)
| Self::Zf023(_)
| Self::Zf027(_)
| Self::Zf085(_)
| Self::Zf077(_)
| Self::Zm035(_)
| Self::Zf088(_)
| Self::Zf024(_)
| Self::Zf072(_)
| Self::Zm055(_)
| Self::Zm052(_)
| Self::Zf071(_)
| Self::Zm061(_)
| Self::Zf078(_)
| Self::Zm013(_)
| Self::Zm081(_)
| Self::Zm037(_)
| Self::Zf090(_)
| Self::Zf043(_)
| Self::Zm058(_)
| Self::Zm012(_)
| Self::Zm045(_)
| Self::Zf075(_)
)
}
pub(super) fn get_speed_v10(&self) -> Result<f32, KokoroError> {
match self {
Self::ZmYunyang(v)
| Self::ZfXiaoni(v)
| Self::AfJessica(v)
| Self::BfLily(v)
| Self::ZfXiaobei(v)
| Self::ZmYunxia(v)
| Self::AfHeart(v)
| Self::BfEmma(v)
| Self::AmPuck(v)
| Self::BfAlice(v)
| Self::HfAlpha(v)
| Self::BfIsabella(v)
| Self::AfNova(v)
| Self::AmFenrir(v)
| Self::EmAlex(v)
| Self::ImNicola(v)
| Self::PmAlex(v)
| Self::AfAlloy(v)
| Self::ZmYunxi(v)
| Self::AfSarah(v)
| Self::JfNezumi(v)
| Self::BmDaniel(v)
| Self::JfTebukuro(v)
| Self::JfAlpha(v)
| Self::JmKumo(v)
| Self::EmSanta(v)
| Self::AmLiam(v)
| Self::AmSanta(v)
| Self::AmEric(v)
| Self::BmFable(v)
| Self::AfBella(v)
| Self::BmLewis(v)
| Self::PfDora(v)
| Self::AfNicole(v)
| Self::BmGeorge(v)
| Self::AmOnyx(v)
| Self::HmPsi(v)
| Self::HfBeta(v)
| Self::HmOmega(v)
| Self::ZfXiaoxiao(v)
| Self::FfSiwis(v)
| Self::EfDora(v)
| Self::AfAoede(v)
| Self::AmEcho(v)
| Self::AmMichael(v)
| Self::AfKore(v)
| Self::ZfXiaoyi(v)
| Self::JfGongitsune(v)
| Self::AmAdam(v)
| Self::IfSara(v)
| Self::AfSky(v)
| Self::PmSanta(v)
| Self::AfRiver(v)
| Self::ZmYunjian(v) => Ok(*v),
_ => Err(KokoroError::VoiceVersionInvalid(
"Expect version 1.0".to_owned(),
)),
}
}
pub(super) fn get_speed_v11(&self) -> Result<i32, KokoroError> {
match self {
Self::Zm029(v)
| Self::Zf048(v)
| Self::Zf008(v)
| Self::Zm014(v)
| Self::Zf003(v)
| Self::Zf047(v)
| Self::Zm080(v)
| Self::Zf094(v)
| Self::Zf046(v)
| Self::Zm054(v)
| Self::Zf001(v)
| Self::Zm062(v)
| Self::BfVale(v)
| Self::Zf044(v)
| Self::Zf005(v)
| Self::Zf028(v)
| Self::Zf059(v)
| Self::Zm030(v)
| Self::Zf074(v)
| Self::Zm009(v)
| Self::Zf004(v)
| Self::Zf021(v)
| Self::Zm095(v)
| Self::Zm041(v)
| Self::Zf087(v)
| Self::Zf039(v)
| Self::Zm031(v)
| Self::Zf007(v)
| Self::Zf038(v)
| Self::Zf092(v)
| Self::Zm056(v)
| Self::Zf099(v)
| Self::Zm010(v)
| Self::Zm069(v)
| Self::Zm016(v)
| Self::Zm068(v)
| Self::Zf083(v)
| Self::Zf093(v)
| Self::Zf006(v)
| Self::Zf026(v)
| Self::Zm053(v)
| Self::Zm064(v)
| Self::AfSol(v)
| Self::Zf042(v)
| Self::Zf084(v)
| Self::Zf073(v)
| Self::Zf067(v)
| Self::Zm025(v)
| Self::Zm020(v)
| Self::Zm050(v)
| Self::Zf070(v)
| Self::Zf002(v)
| Self::Zf032(v)
| Self::Zm091(v)
| Self::Zm066(v)
| Self::Zm089(v)
| Self::Zm034(v)
| Self::Zm100(v)
| Self::Zf086(v)
| Self::Zf040(v)
| Self::Zm011(v)
| Self::Zm098(v)
| Self::Zm015(v)
| Self::Zf051(v)
| Self::Zm065(v)
| Self::Zf076(v)
| Self::Zf036(v)
| Self::Zm033(v)
| Self::Zf018(v)
| Self::Zf017(v)
| Self::Zf049(v)
| Self::AfMaple(v)
| Self::Zm082(v)
| Self::Zm057(v)
| Self::Zf079(v)
| Self::Zf022(v)
| Self::Zm063(v)
| Self::Zf060(v)
| Self::Zf019(v)
| Self::Zm097(v)
| Self::Zm096(v)
| Self::Zf023(v)
| Self::Zf027(v)
| Self::Zf085(v)
| Self::Zf077(v)
| Self::Zm035(v)
| Self::Zf088(v)
| Self::Zf024(v)
| Self::Zf072(v)
| Self::Zm055(v)
| Self::Zm052(v)
| Self::Zf071(v)
| Self::Zm061(v)
| Self::Zf078(v)
| Self::Zm013(v)
| Self::Zm081(v)
| Self::Zm037(v)
| Self::Zf090(v)
| Self::Zf043(v)
| Self::Zm058(v)
| Self::Zm012(v)
| Self::Zm045(v)
| Self::Zf075(v) => Ok(*v),
_ => Err(KokoroError::VoiceVersionInvalid(
"Expect version 1.1".to_owned(),
)),
}
}
}