mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 16:38:14 +00:00
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:
1 parent
3a6b201ae0
commit
f86f6aa3ea
13 files changed
+414
-46
No files matched your search
Generated
+1
@@ -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)",
|
||||
|
||||
@@ -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.*"
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2016 Brandon Thomas <bt@brand.io, echelon@gmail.com>
|
||||
|
||||
pub mod arpabet;
|
||||
pub mod dictionary;
|
||||
pub mod tokenizer;
|
||||
|
||||
@@ -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
@@ -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.
File renamed without changes.
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user