Foundations for sentence tokenization (WIP)

- Sentence parsing and correction moved into a single module
- Typesafe "Speaker" parameter
- Regex + lazy_static!
This commit is contained in:
Brandon Thomas committed 2016-10-02 15:28:48 -04:00
1 parent 3a6b201ae0
commit f86f6aa3ea
13 files changed
+414 -46

No files matched your search

Generated
+1
View File
@@ -6,6 +6,7 @@ dependencies = [
"hound 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.9.10 (registry+https://github.com/rust-lang/crates.io-index)",
"iron 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"params 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.1.73 (registry+https://github.com/rust-lang/crates.io-index)",
+1
View File
@@ -13,6 +13,7 @@
hound = "1.1.0" # WAV codec.
hyper = "0.9.*"
iron = "0.4.*"
lazy_static = "0.2.*"
log = "0.3.*"
params = "0.3.*"
regex = "0.1.*"
+6 -5
View File
@@ -1,7 +1,6 @@
// Copyright (c) 2015 Brandon Thomas <bt@brand.io>
// TODO: This looks really bad now. Needs cleanup.
use std::collections::HashMap;
use config::Config;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
@@ -11,8 +10,11 @@ use iron::headers::{ETag, EntityTag, Headers, IfNoneMatch};
use iron::mime::Mime;
use iron::prelude::*;
use iron::status;
use old_words::split_sentence;
use router::Router;
use rustc_serialize::json;
use speaker::Speaker;
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{self, Debug};
use std::fs::File;
@@ -30,7 +32,6 @@ use std::sync::RwLock;
use super::error_filter::build_error;
use synthesizer::Synthesizer;
use urlencoded::UrlEncodedQuery;
use words::split_sentence;
const SENTENCE_PARAM : &'static str = "s";
const SPEAKER_PARAM : &'static str = "v";
@@ -56,7 +57,7 @@ struct SpeakRequest {
pub sentence: String,
/** The voice to use to render the audio. */
pub speaker: String,
pub speaker: Speaker,
/** An optional volume multiplier. */
pub volume: Option<f32>,
@@ -143,7 +144,7 @@ impl SpeakRequest {
Some(list) => {
match list.get(0) {
None => { return speaker_error; },
Some(s) => { s.to_string() },
Some(s) => { Speaker::new(s.to_string()) },
}
},
};
@@ -386,7 +387,7 @@ impl AudioSynthHandler {
fn sha_digest(&self, request: &SpeakRequest) -> String {
let mut hasher = Sha1::new();
hasher.input_str(&request.speaker);
hasher.input_str(&request.speaker.to_string());
hasher.input_str(&request.sentence);
if request.volume.is_some() {
+1 -1
View File
@@ -1,10 +1,10 @@
// Copyright (c) 2015 Brandon Thomas <bt@brand.io>
use dictionary::VocabularyLibrary;
use iron::Handler;
use iron::mime::Mime;
use iron::prelude::*;
use iron::status;
use old_dictionary::VocabularyLibrary;
use rustc_serialize::json;
use std::path::Path;
use std::path::PathBuf;
+20 -8
View File
@@ -1,27 +1,29 @@
// Copyright (c) 2016 Brandon Thomas <bt@brand.io, echelon@gmail.com>
use super::dictionary::Dictionary;
use error::SynthError;
use std::collections::HashMap;
use regex::Regex;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs::File;
use std::io::BufReader;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
pub type Word = String;
pub type Phoneme = String;
pub type Polyphone = Vec<Phoneme>;
pub struct ArpabetDictionary {
pub struct Arpabet {
/// A map of lowercase words to polyphone breakdown (phones are uppercase).
/// eg. 'jungle' -> [JH, AH1, NG, G, AH0, L]
dictionary: HashMap<Word, Polyphone>
}
impl ArpabetDictionary {
impl Arpabet {
/// Read CMU's Arpabet Dictionary.
/// See http://www.speech.cs.cmu.edu/cgi-bin/cmudict
pub fn load_from_file(filename: &str) -> Result<ArpabetDictionary, SynthError> {
pub fn load_from_file(filename: &str) -> Result<Arpabet, SynthError> {
let mut f = try!(File::open(filename));
let mut reader = BufReader::new(f);
@@ -54,7 +56,7 @@ impl ArpabetDictionary {
if map.is_empty() {
Err(SynthError::EmptyFile)
} else {
Ok(ArpabetDictionary { dictionary: map })
Ok(Arpabet { dictionary: map })
}
}
@@ -66,15 +68,25 @@ impl ArpabetDictionary {
}
/// Combine two dictionaries into a new one.
pub fn combine(&self, other: &ArpabetDictionary) -> ArpabetDictionary {
pub fn combine(&self, other: &Arpabet) -> Arpabet {
let mut combined = self.dictionary.clone();
for (k, v) in other.dictionary.iter() {
combined.insert(k.clone(), v.clone());
}
ArpabetDictionary {
Arpabet {
dictionary: combined
}
}
/// Extract and return the internal word dictionary (a list of all
/// the words contained in the arpabet).
pub fn to_dictionary(&self) -> Dictionary {
let mut words = HashSet::new();
for word in self.dictionary.keys() {
words.insert(word.to_string());
}
Dictionary::new(words)
}
}
+126
View File
@@ -0,0 +1,126 @@
// Copyright (c) 2016 Brandon Thomas <bt@brand.io, echelon@gmail.com>
use speaker::Speaker;
use std::collections::HashSet;
use std::collections::HashMap;
pub type Word = String;
pub struct Dictionary {
/// Words that exist in this dictionary.
words: HashSet<Word>,
}
pub struct UniversalDictionary {
/// There is a single arpabet dictionary that works for all speakers.
arpabet_dictionary: Dictionary,
/// There are speaker-specific word dictionaries.
speaker_dictionaries: HashMap<Speaker, Dictionary>,
}
impl Dictionary {
pub fn new(words: HashSet<Word>) -> Dictionary {
Dictionary { words: words }
}
pub fn empty() -> Dictionary {
Dictionary { words: HashSet::new() }
}
/// Returns true if the dictionary contains the word.
pub fn contains(&self, word: &str) -> bool {
self.words.contains(word)
}
fn insert(mut self, word: &str) -> Self {
self.words.insert(word.to_string());
self
}
}
impl UniversalDictionary {
pub fn new() -> UniversalDictionary {
UniversalDictionary {
arpabet_dictionary: Dictionary::empty(),
speaker_dictionaries: HashMap::new(),
}
}
pub fn set_arpabet_dictionary(&mut self, arpabet_dictionary: Dictionary)
-> &mut Self {
self.arpabet_dictionary = arpabet_dictionary;
self
}
pub fn set_speaker_dictionary(&mut self,
speaker: Speaker,
dictionary: Dictionary)
-> &mut Self {
self.speaker_dictionaries.insert(speaker, dictionary);
self
}
/// Returns true if anyone can say the word.
pub fn contains(&self, word: &str) -> bool {
self.arpabet_dictionary.contains(word)
}
/// Returns true if the speaker can say the word.
pub fn contains_for(&self, speaker: &Speaker, word: &str) -> bool {
self.arpabet_dictionary.contains(word) ||
self.speaker_dictionaries.get(speaker)
.map(|dict| { dict.contains(word) })
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use speaker::Speaker;
use super::*;
#[test]
fn universal_dictionary_contains_for_speaker() {
let alice = Speaker::new("alice".to_string());
let bob = Speaker::new("bob".to_string());
let arpa = Dictionary::empty()
.insert("foo")
.insert("bar");
let alice_dict = Dictionary::empty()
.insert("111")
.insert("222");
let bob_dict = Dictionary::empty()
.insert("aaa")
.insert("bbb");
let mut uni = UniversalDictionary::new();
uni.set_arpabet_dictionary(arpa)
.set_speaker_dictionary(alice.clone(), alice_dict)
.set_speaker_dictionary(bob.clone(), bob_dict);
assert_eq!(true, uni.contains_for(&alice, "foo"));
assert_eq!(true, uni.contains_for(&alice, "bar"));
assert_eq!(false, uni.contains_for(&alice, "baz"));
assert_eq!(true, uni.contains_for(&alice, "111"));
assert_eq!(true, uni.contains_for(&alice, "222"));
assert_eq!(false, uni.contains_for(&alice, "aaa"));
assert_eq!(false, uni.contains_for(&alice, "bbb"));
assert_eq!(true, uni.contains_for(&bob, "foo"));
assert_eq!(false, uni.contains_for(&bob, "111"));
assert_eq!(true, uni.contains_for(&bob, "aaa"));
assert_eq!(false, uni.contains_for(&bob, "absent"));
let nobody = Speaker::new("nobody".to_string());
assert_eq!(true, uni.contains_for(&nobody, "foo"));
assert_eq!(false, uni.contains_for(&nobody, "111"));
assert_eq!(false, uni.contains_for(&nobody, "aaa"));
assert_eq!(false, uni.contains_for(&nobody, "absent"));
}
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright (c) 2016 Brandon Thomas <bt@brand.io, echelon@gmail.com>
pub mod arpabet;
pub mod dictionary;
pub mod tokenizer;
+154
View File
@@ -0,0 +1,154 @@
// Copyright (c) 2016 Brandon Thomas <bt@brand.io, echelon@gmail.com>
// TODO: This doesn't land the tokenization I want, but it'll be iterative.
use regex::Regex;
use speaker::Speaker;
use std::sync::Arc;
//use super::dictionary::UniversalDictionary;
lazy_static! {
// Note: Rust regexes do not support lookaround.
// Designed not to match times (5:00) or URLs (http://)
pub static ref RE_ALPHA_COLON: Regex = Regex::new(r"([A-Za-z]):([^/])").unwrap();
pub static ref RE_BEGIN_SINGLE_QUOTE: Regex = Regex::new("^'").unwrap();
pub static ref RE_PERIOD_END: Regex = Regex::new("\\.$").unwrap();
pub static ref RE_PERIOD_SPACE: Regex = Regex::new("\\.\\s").unwrap();
pub static ref RE_SINGLE_QUOTE_END: Regex = Regex::new("'$").unwrap();
pub static ref RE_SINGLE_QUOTE_SPACE: Regex = Regex::new("'\\s").unwrap();
pub static ref RE_SPACE_SINGLE_QUOTE: Regex = Regex::new("\\s'").unwrap();
pub static ref RE_URL: Regex = Regex::new(r"https?://[\w\.-]+/?(\w+)?").unwrap();
}
//pub type TokenizedSentence = String;
pub struct Tokenizer {
// TODO: Use dictionary.
///// The dictionary used to check if a word exists.
//dictionary: Arc<UniversalDictionary>,
}
impl Tokenizer {
/*// TODO
pub fn tokenize_sentence(&self,
speaker: &Speaker,
raw_sentence: &str) -> TokenizedSentence */
// TODO: Make only for testing.
/// Only for testing.
pub fn empty() -> Tokenizer {
//Tokenizer { dictionary: Arc::new(UniversalDictionary::new()) }
Tokenizer {}
}
pub fn convert(&self, speaker: &Speaker, raw_sentence: &str) -> String {
let mut sentence = raw_sentence.to_string();
sentence = RE_ALPHA_COLON.replace_all(&sentence, "$1 ");
sentence = RE_BEGIN_SINGLE_QUOTE.replace_all(&sentence, "");
sentence = RE_PERIOD_END.replace_all(&sentence, "");
sentence = RE_PERIOD_SPACE.replace_all(&sentence, " ");
sentence = RE_SINGLE_QUOTE_END.replace_all(&sentence, "");
sentence = RE_SINGLE_QUOTE_SPACE.replace_all(&sentence, " ");
sentence = RE_SPACE_SINGLE_QUOTE.replace_all(&sentence, " ");
// Replace &, @, <, >, etc.
sentence = sentence.replace("&", " and ")
.replace("@", " at ")
.replace("<", " less than ")
.replace(">", " greater than ");
// Remove URLs
sentence = RE_URL.replace_all(&sentence, " ");
// Remove characters.
sentence = sentence.replace("\"", "")
.replace(",", "")
.replace("!", "")
.replace("?", "")
.replace("--", " ")
.replace(" - ", " ")
.replace("...", " ")
.replace("…", " ");
// Remove extra spaces
// Lowercase
let split = split_sentence(&sentence);
sentence = split.join(" ");
info!("Final sentence: {}", sentence);
sentence
}
}
/// Split a sentence into words. Remove extra padding, etc.
fn split_sentence(sentence: &str) -> Vec<String> {
let mut words = Vec::new();
let split = sentence.split(char::is_whitespace);
for s in split {
let trim = s.trim();
if trim.len() == 0 { continue; }
words.push(trim.to_lowercase());
}
words
}
#[cfg(test)]
mod tests {
use speaker::Speaker;
use super::*;
use super::split_sentence;
#[test]
fn test_prepare() {
let t = Tokenizer::empty();
let s = Speaker::new("speaker".to_string());
// Normalize spaces
assert_eq!("foo bar baz", &t.convert(&s, " FOO BAR BAZ "));
// Convert symbolic words
assert_eq!("fox and hound", &t.convert(&s, "Fox & Hound"));
assert_eq!("thing at place", &t.convert(&s, "thing @ place"));
assert_eq!("me greater than you", &t.convert(&s, "me > you"));
assert_eq!("this less than that", &t.convert(&s, "this < that"));
// Drop unimportant characters
assert_eq!("it's a quote", &t.convert(&s, "'It's a quote'"));
assert_eq!("quote quote", &t.convert(&s, "\"Quote quote\""));
// Drop URLs
assert_eq!("testing link", &t.convert(&s, "Testing https://t.co/1A2b3cdEfG link"));
assert_eq!("visit", &t.convert(&s, "Visit https://t.co/A1A1b0b0b0…"));
// Complex examples taken from real tweets.
assert_eq!("will be in novi michigan this friday at 5:00pm join the movement tickets available at",
&t.convert(&s, r#"Will be in Novi, Michigan this Friday at 5:00pm. Join the MOVEMENT!
Tickets available at: https://t.co/Q6APf0ZFYA… https://t.co/6WAyO9eQHN"#));
// TODO
// Handle numerics
//assert_eq!("u.s. murders increased ten point eight percent in twenty fifteen",
// &t.convert(&s, "U.S. Murders Increased 10.8% in 2015"));
// TODO: WAAAY MORE TESTS.
}
#[test]
fn test_split_sentence() {
assert_eq!(sen(&["foo"]), split_sentence(" foo "));
assert_eq!(sen(&["foo", "bar", "baz"]), split_sentence("foo bar baz"));
assert_eq!(sen(&[".", "..", "..."]), split_sentence(" .\n..\t\t...\n "));
let empty : Vec<String> = Vec::new();
assert_eq!(empty, split_sentence(" \n\t "));
}
fn sen(list: &[&str]) -> Vec<String> {
let mut out = Vec::new();
for it in list { out.push(it.to_string()); }
out
}
}
+16 -11
View File
@@ -1,11 +1,12 @@
// Copyright (c) 2015-2016 Brandon Thomas <bt@brand.io>
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate log;
extern crate clap;
extern crate crypto;
extern crate hound;
extern crate iron;
#[macro_use]
extern crate log;
extern crate regex;
extern crate resolve;
extern crate router;
@@ -14,28 +15,30 @@ extern crate time;
extern crate toml;
extern crate urlencoded;
pub mod arpabet;
pub mod audiobank;
pub mod config;
pub mod dictionary;
pub mod effects;
pub mod error;
pub mod handlers;
pub mod lang;
pub mod logger;
pub mod old_dictionary;
pub mod old_words;
pub mod speaker;
pub mod synthesizer;
pub mod words;
use arpabet::ArpabetDictionary;
use audiobank::Audiobank;
use clap::{App, Arg, ArgMatches};
use config::Config;
use dictionary::VocabularyLibrary;
use handlers::audio_synth_handler::AudioSynthHandler;
use handlers::error_filter::ErrorFilter;
use handlers::file_server_handler::FileServerHandler;
use handlers::vocab_list_handler::VocabListHandler;
use iron::prelude::*;
use lang::arpabet::Arpabet;
use lang::tokenizer::Tokenizer;
use logger::SimpleLogger;
use old_dictionary::VocabularyLibrary;
use resolve::hostname;
use router::Router;
use std::path::Path;
@@ -117,15 +120,15 @@ fn get_hostname() {
fn create_synthesizer(config: &Config) -> Synthesizer {
info!("Reading Arpabet Dictionary...");
let arpabet_dictionary = ArpabetDictionary::load_from_file(
let arpabet_dictionary = Arpabet::load_from_file(
&config.phoneme_dictionary_file_development).unwrap();
info!("Reading Extra Dictionary...");
let extra_dictionary = ArpabetDictionary::load_from_file(
let extra_dictionary = Arpabet::load_from_file(
&config.extra_dictionary_file_development).unwrap();
info!("Reading Square Dictionary...");
let square_dictionary = ArpabetDictionary::load_from_file(
let square_dictionary = Arpabet::load_from_file(
&config.square_dictionary_file_development).unwrap();
let dictionary = arpabet_dictionary
@@ -134,7 +137,9 @@ fn create_synthesizer(config: &Config) -> Synthesizer {
let audiobank = Audiobank::new(&config.get_sound_path());
let tokenizer = Tokenizer::empty();
info!("Building Synthesizer...");
Synthesizer::new(dictionary, audiobank)
Synthesizer::new(dictionary, audiobank, tokenizer)
}
File renamed without changes.
View File
File renamed without changes.
+54
View File
@@ -0,0 +1,54 @@
// Copyright (c) 2016 Brandon Thomas <bt@brand.io, echelon@gmail.com>
use std::fmt;
/// Simply for type safety.
/// Don't do stupid things with this.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Speaker {
pub speaker: String,
}
impl Speaker {
pub fn new(speaker: String) -> Speaker {
Speaker { speaker: speaker }
}
pub fn as_str(&self) -> &str {
&self.speaker
}
/// Don't overuse.
pub fn to_string(&self) -> String {
self.speaker.clone()
}
}
impl fmt::Display for Speaker {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.speaker)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_speaker() {
let alice = Speaker::new("alice".to_string());
let bob = Speaker::new("bob".to_string());
assert_eq!(alice, Speaker::new("alice".to_string()));
assert_eq!(&alice, &Speaker::new("alice".to_string()));
assert!(alice == alice);
assert!(alice == Speaker::new("alice".to_string()));
assert!(&alice == &alice);
assert!(&alice == &Speaker::new("alice".to_string()));
assert!(alice != bob);
assert!(&alice != &bob);
}
}
+29 -21
View File
@@ -1,6 +1,5 @@
// Copyright (c) 2016 Brandon Thomas <bt@brand.io, echelon@gmail.com>
use arpabet::ArpabetDictionary;
use audiobank::Audiobank;
use audiobank::SampleBytes;
use audiobank::SamplePreference;
@@ -10,9 +9,12 @@ use effects::volume::change_volume;
use error::SynthError;
use hound::WavSpec;
use hound::WavWriter;
use lang::arpabet::Arpabet;
use lang::tokenizer::Tokenizer;
use old_words::split_sentence;
use speaker::Speaker;
use std::io::BufWriter;
use std::io::Cursor;
use words::split_sentence;
pub type WavBytes = Vec<u8>;
@@ -25,20 +27,25 @@ pub struct Synthesizer {
///// Word dictionary
//word_dictionary: Vocabulary,
/// Arpabet dictionary, (Word) -> (PhonemeList)
arpabet_dictionary: ArpabetDictionary,
/// Arpabet, (Word) -> (PhonemeList)
arpabet_dictionary: Arpabet,
/// WAV Audiobank for sound generation.
audiobank: Audiobank,
/// Sentence tokenizer
tokenizer: Tokenizer,
}
impl Synthesizer {
/// CTOR
pub fn new(arpabet_dictionary: ArpabetDictionary, audiobank: Audiobank)
-> Synthesizer {
pub fn new(arpabet_dictionary: Arpabet,
audiobank: Audiobank,
tokenizer: Tokenizer) -> Synthesizer {
Synthesizer {
arpabet_dictionary: arpabet_dictionary,
audiobank: audiobank,
tokenizer: tokenizer,
}
}
@@ -47,7 +54,7 @@ impl Synthesizer {
*/
pub fn generate(&self,
sentence: &str,
speaker: &str,
speaker: &Speaker,
use_words: bool,
use_phonemes: bool,
use_diphones: bool,
@@ -62,7 +69,8 @@ impl Synthesizer {
word_padding_end: Option<u16>)
-> Result<WavBytes, SynthError> {
let mut words = split_sentence(sentence);
let prepared = self.tokenizer.convert(speaker, sentence);
let mut words = split_sentence(&prepared);
if words.len() == 0 {
return Err(SynthError::BadInput {
@@ -137,11 +145,11 @@ impl Synthesizer {
/// whether or not the word was successfully found and concatenated.
fn concatenate_word(&self,
concatenated_waveform: &mut Vec<i16>,
speaker: &str,
speaker: &Speaker,
word: &str,
word_padding_start: Option<u16>,
word_padding_end: Option<u16>) -> bool {
match self.audiobank.get_word(speaker, word) {
match self.audiobank.get_word(speaker.as_str(), word) {
None => {
// The word does not exist in the database.
false
@@ -170,7 +178,7 @@ impl Synthesizer {
/// successfully found and concatenated.
fn concatenate_polyphone(&self,
concatenated_waveform: &mut Vec<i16>,
speaker: &str,
speaker: &Speaker,
word: &str,
use_diphones: bool,
monophone_padding_start: Option<u16>,
@@ -217,7 +225,7 @@ impl Synthesizer {
if use_diphones && i < end {
let first = phoneme;
let second = &polyphone[i+1];
match self.audiobank.get_diphone(speaker, first, second) {
match self.audiobank.get_diphone(speaker.as_str(), first, second) {
None => {},
Some(diphone_data) => {
info!("Read diphone: {}, {}", first, second);
@@ -230,16 +238,16 @@ impl Synthesizer {
// Attempt to read a "begin" or "end" monophone.
let mut read_results = if i == 0 {
self.audiobank.get_begin_phoneme(speaker, &phoneme)
self.audiobank.get_begin_phoneme(speaker.as_str(), &phoneme)
} else if i == end {
self.audiobank.get_end_phoneme(speaker, &phoneme)
self.audiobank.get_end_phoneme(speaker.as_str(), &phoneme)
} else {
None
};
// Read a regular monophone.
if read_results.is_none() {
read_results = self.audiobank.get_phoneme(speaker, &phoneme);
read_results = self.audiobank.get_phoneme(speaker.as_str(), &phoneme);
}
match read_results {
@@ -282,7 +290,7 @@ impl Synthesizer {
// TODO: Doc and test.
fn concatenate_n_phone(&self,
concatenated_waveform: &mut Vec<i16>,
speaker: &str,
speaker: &Speaker,
word: &str,
polyphone_padding_end: Option<u16>,
use_ends: bool) -> bool {
@@ -314,7 +322,7 @@ impl Synthesizer {
// TODO: Make this super easy to test (and write some tests).
// TODO: Make this super efficient.
fn get_n_phone_samples(&self,
speaker: &str,
speaker: &Speaker,
word: &str,
sample_preference: SamplePreference,
use_ends: bool)
@@ -361,7 +369,7 @@ impl Synthesizer {
};
//println!("n-phone: {:?}", candidate_n_phone);
let phone = self.audiobank.get_n_phone(speaker,
let phone = self.audiobank.get_n_phone(speaker.as_str(),
candidate_n_phone,
sample_preference,
use_ends);
@@ -395,7 +403,7 @@ impl Synthesizer {
_ => { SamplePreference::Middle },
};
let phone = self.audiobank.get_n_phone(speaker,
let phone = self.audiobank.get_n_phone(speaker.as_str(),
candidate_n_phone,
sample_preference,
use_ends);
@@ -428,7 +436,7 @@ impl Synthesizer {
_ => { SamplePreference::Middle },
};
let phone = self.audiobank.get_n_phone(speaker,
let phone = self.audiobank.get_n_phone(speaker.as_str(),
candidate_n_phone,
sample_preference,
use_ends);
@@ -457,7 +465,7 @@ impl Synthesizer {
_ => { SamplePreference::Middle },
};
let phone = self.audiobank.get_n_phone(speaker,
let phone = self.audiobank.get_n_phone(speaker.as_str(),
&polyphone[i..i+1],
sample_preference,
use_ends);