mirror of https://github.com/ndarilek/tts-rs.git
Add voices support to web platform.
This commit is contained in:
parent
3f9e7c22db
commit
e699f7e5e5
|
@ -41,7 +41,7 @@ objc = { version = "0.2", features = ["exception"] }
|
||||||
|
|
||||||
[target.wasm32-unknown-unknown.dependencies]
|
[target.wasm32-unknown-unknown.dependencies]
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisErrorCode", "SpeechSynthesisErrorEvent", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "Window", ] }
|
web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisErrorCode", "SpeechSynthesisErrorEvent", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "SpeechSynthesisVoice", "Window", ] }
|
||||||
|
|
||||||
[target.'cfg(target_os="android")'.dependencies]
|
[target.'cfg(target_os="android")'.dependencies]
|
||||||
jni = "0.19"
|
jni = "0.19"
|
||||||
|
|
|
@ -7,5 +7,7 @@ edition = "2018"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
console_log = "0.2"
|
||||||
|
log = "0.4"
|
||||||
seed = "0.8"
|
seed = "0.8"
|
||||||
tts = { path = "../.." }
|
tts = { path = "../.." }
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>Example</title>
|
<title>Example</title>
|
||||||
|
|
|
@ -15,13 +15,20 @@ enum Msg {
|
||||||
RateChanged(String),
|
RateChanged(String),
|
||||||
PitchChanged(String),
|
PitchChanged(String),
|
||||||
VolumeChanged(String),
|
VolumeChanged(String),
|
||||||
|
VoiceChanged(String),
|
||||||
Speak,
|
Speak,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
|
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
|
||||||
let tts = Tts::default().unwrap();
|
let mut tts = Tts::default().unwrap();
|
||||||
|
if tts.voices().unwrap().iter().len() > 0 {
|
||||||
|
if tts.voice().unwrap().is_none() {
|
||||||
|
tts.set_voice(tts.voices().unwrap().first().unwrap())
|
||||||
|
.expect("Failed to set voice");
|
||||||
|
}
|
||||||
|
}
|
||||||
Model {
|
Model {
|
||||||
text: Default::default(),
|
text: "Hello, world. This is a test of the current text-to-speech values.".into(),
|
||||||
tts,
|
tts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,6 +49,13 @@ fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
|
||||||
let volume = volume.parse::<f32>().unwrap();
|
let volume = volume.parse::<f32>().unwrap();
|
||||||
model.tts.set_volume(volume).unwrap();
|
model.tts.set_volume(volume).unwrap();
|
||||||
}
|
}
|
||||||
|
VoiceChanged(voice) => {
|
||||||
|
for v in model.tts.voices().unwrap() {
|
||||||
|
if v.id() == voice {
|
||||||
|
model.tts.set_voice(&v).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Speak => {
|
Speak => {
|
||||||
model.tts.speak(&model.text, false).unwrap();
|
model.tts.speak(&model.text, false).unwrap();
|
||||||
}
|
}
|
||||||
|
@ -49,6 +63,7 @@ fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(model: &Model) -> Node<Msg> {
|
fn view(model: &Model) -> Node<Msg> {
|
||||||
|
let should_show_voices = model.tts.voices().unwrap().iter().len() > 0;
|
||||||
form![
|
form![
|
||||||
div![label![
|
div![label![
|
||||||
"Text to speak",
|
"Text to speak",
|
||||||
|
@ -96,6 +111,36 @@ fn view(model: &Model) -> Node<Msg> {
|
||||||
input_ev(Ev::Input, Msg::VolumeChanged)
|
input_ev(Ev::Input, Msg::VolumeChanged)
|
||||||
],
|
],
|
||||||
],],
|
],],
|
||||||
|
if should_show_voices {
|
||||||
|
div![
|
||||||
|
label!["Voice"],
|
||||||
|
select![
|
||||||
|
model.tts.voices().unwrap().iter().map(|v| {
|
||||||
|
let selected = if let Some(voice) = model.tts.voice().unwrap() {
|
||||||
|
voice.id() == v.id()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
option![
|
||||||
|
attrs! {
|
||||||
|
At::Value => v.id()
|
||||||
|
},
|
||||||
|
if selected {
|
||||||
|
attrs! {
|
||||||
|
At::Selected => selected
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attrs! {}
|
||||||
|
},
|
||||||
|
v.name()
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
input_ev(Ev::Change, Msg::VoiceChanged)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
div!["Your browser does not seem to support selecting voices."]
|
||||||
|
},
|
||||||
button![
|
button![
|
||||||
"Speak",
|
"Speak",
|
||||||
ev(Ev::Click, |e| {
|
ev(Ev::Click, |e| {
|
||||||
|
@ -107,5 +152,6 @@ fn view(model: &Model) -> Node<Msg> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
console_log::init().expect("Error initializing logger");
|
||||||
App::start("app", init, update, view);
|
App::start("app", init, update, view);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use std::sync::Mutex;
|
use std::{str::FromStr, sync::Mutex};
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
|
use unic_langid::LanguageIdentifier;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::{
|
use web_sys::{
|
||||||
SpeechSynthesisErrorCode, SpeechSynthesisErrorEvent, SpeechSynthesisEvent,
|
SpeechSynthesisErrorCode, SpeechSynthesisErrorEvent, SpeechSynthesisEvent,
|
||||||
SpeechSynthesisUtterance,
|
SpeechSynthesisUtterance, SpeechSynthesisVoice,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::Gender;
|
||||||
|
use crate::Voice;
|
||||||
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
|
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -18,6 +21,7 @@ pub struct Web {
|
||||||
rate: f32,
|
rate: f32,
|
||||||
pitch: f32,
|
pitch: f32,
|
||||||
volume: f32,
|
volume: f32,
|
||||||
|
voice: Option<SpeechSynthesisVoice>,
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -35,6 +39,7 @@ impl Web {
|
||||||
rate: 1.,
|
rate: 1.,
|
||||||
pitch: 1.,
|
pitch: 1.,
|
||||||
volume: 1.,
|
volume: 1.,
|
||||||
|
voice: None,
|
||||||
};
|
};
|
||||||
*backend_id += 1;
|
*backend_id += 1;
|
||||||
Ok(rv)
|
Ok(rv)
|
||||||
|
@ -53,7 +58,8 @@ impl Backend for Web {
|
||||||
pitch: true,
|
pitch: true,
|
||||||
volume: true,
|
volume: true,
|
||||||
is_speaking: true,
|
is_speaking: true,
|
||||||
voices: true,
|
voice: true,
|
||||||
|
get_voice: true,
|
||||||
utterance_callbacks: true,
|
utterance_callbacks: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,6 +70,9 @@ impl Backend for Web {
|
||||||
utterance.set_rate(self.rate);
|
utterance.set_rate(self.rate);
|
||||||
utterance.set_pitch(self.pitch);
|
utterance.set_pitch(self.pitch);
|
||||||
utterance.set_volume(self.volume);
|
utterance.set_volume(self.volume);
|
||||||
|
if self.voice.is_some() {
|
||||||
|
utterance.set_voice(self.voice.as_ref());
|
||||||
|
}
|
||||||
let id = self.id().unwrap();
|
let id = self.id().unwrap();
|
||||||
let mut uid = NEXT_UTTERANCE_ID.lock().unwrap();
|
let mut uid = NEXT_UTTERANCE_ID.lock().unwrap();
|
||||||
let utterance_id = UtteranceId::Web(*uid);
|
let utterance_id = UtteranceId::Web(*uid);
|
||||||
|
@ -198,16 +207,53 @@ impl Backend for Web {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn voice(&self) -> Result<String, Error> {
|
fn voice(&self) -> Result<Option<Voice>, Error> {
|
||||||
unimplemented!()
|
if let Some(voice) = &self.voice {
|
||||||
|
Ok(Some(voice.clone().into()))
|
||||||
|
} else {
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
let speech_synthesis = window.speech_synthesis().unwrap();
|
||||||
|
for voice in speech_synthesis.get_voices().iter() {
|
||||||
|
let voice: SpeechSynthesisVoice = voice.into();
|
||||||
|
if voice.default() {
|
||||||
|
return Ok(Some(voice.into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::NoneError);
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_voices(&self) -> Vec<String> {
|
fn voices(&self) -> Result<Vec<Voice>, Error> {
|
||||||
unimplemented!()
|
if let Some(window) = web_sys::window() {
|
||||||
|
let speech_synthesis = window.speech_synthesis().unwrap();
|
||||||
|
let mut rv: Vec<Voice> = vec![];
|
||||||
|
for v in speech_synthesis.get_voices().iter() {
|
||||||
|
let v: SpeechSynthesisVoice = v.into();
|
||||||
|
rv.push(v.into());
|
||||||
|
}
|
||||||
|
Ok(rv)
|
||||||
|
} else {
|
||||||
|
Err(Error::NoneError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_voice(&mut self, voice: &str) -> Result<(), Error> {
|
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> {
|
||||||
unimplemented!()
|
if let Some(window) = web_sys::window() {
|
||||||
|
let speech_synthesis = window.speech_synthesis().unwrap();
|
||||||
|
for v in speech_synthesis.get_voices().iter() {
|
||||||
|
let v: SpeechSynthesisVoice = v.into();
|
||||||
|
if v.voice_uri() == voice.id {
|
||||||
|
self.voice = Some(v);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(Error::OperationFailed);
|
||||||
|
} else {
|
||||||
|
Err(Error::NoneError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,3 +263,15 @@ impl Drop for Web {
|
||||||
mappings.retain(|v| v.0 != self.id);
|
mappings.retain(|v| v.0 != self.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<SpeechSynthesisVoice> for Voice {
|
||||||
|
fn from(other: SpeechSynthesisVoice) -> Self {
|
||||||
|
let language = LanguageIdentifier::from_str(&other.lang()).unwrap();
|
||||||
|
Voice {
|
||||||
|
id: other.voice_uri(),
|
||||||
|
name: other.name(),
|
||||||
|
gender: Gender::Unspecified,
|
||||||
|
language,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
32
src/lib.rs
32
src/lib.rs
|
@ -237,7 +237,7 @@ pub trait Backend: Clone {
|
||||||
fn set_volume(&mut self, volume: f32) -> Result<(), Error>;
|
fn set_volume(&mut self, volume: f32) -> Result<(), Error>;
|
||||||
fn is_speaking(&self) -> Result<bool, Error>;
|
fn is_speaking(&self) -> Result<bool, Error>;
|
||||||
fn voices(&self) -> Result<Vec<Voice>, Error>;
|
fn voices(&self) -> Result<Vec<Voice>, Error>;
|
||||||
fn voice(&self) -> Result<Voice, Error>;
|
fn voice(&self) -> Result<Option<Voice>, Error>;
|
||||||
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error>;
|
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -582,7 +582,7 @@ impl Tts {
|
||||||
/**
|
/**
|
||||||
* Return the current speaking voice.
|
* Return the current speaking voice.
|
||||||
*/
|
*/
|
||||||
pub fn voice(&self) -> Result<Voice, Error> {
|
pub fn voice(&self) -> Result<Option<Voice>, Error> {
|
||||||
let Features { get_voice, .. } = self.supported_features();
|
let Features { get_voice, .. } = self.supported_features();
|
||||||
if get_voice {
|
if get_voice {
|
||||||
self.0.read().unwrap().voice()
|
self.0.read().unwrap().voice()
|
||||||
|
@ -702,7 +702,7 @@ impl Drop for Tts {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum Gender {
|
pub enum Gender {
|
||||||
Unspecified,
|
Unspecified,
|
||||||
Male,
|
Male,
|
||||||
|
@ -711,8 +711,26 @@ pub enum Gender {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Voice {
|
pub struct Voice {
|
||||||
pub id: String,
|
pub(crate) id: String,
|
||||||
pub name: String,
|
pub(crate) name: String,
|
||||||
pub gender: Gender,
|
pub(crate) gender: Gender,
|
||||||
pub language: LanguageIdentifier,
|
pub(crate) language: LanguageIdentifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Voice {
|
||||||
|
pub fn id(&self) -> String {
|
||||||
|
self.id.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> String {
|
||||||
|
self.name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gender(&self) -> Gender {
|
||||||
|
self.gender
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn language(&self) -> LanguageIdentifier {
|
||||||
|
self.language.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue