mirror of
https://github.com/ndarilek/tts-rs.git
synced 2024-11-18 21:29:37 +00:00
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]
|
||||
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]
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
seed = "0.8"
|
||||
tts = { path = "../.." }
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Example</title>
|
||||
|
|
|
@ -15,13 +15,20 @@ enum Msg {
|
|||
RateChanged(String),
|
||||
PitchChanged(String),
|
||||
VolumeChanged(String),
|
||||
VoiceChanged(String),
|
||||
Speak,
|
||||
}
|
||||
|
||||
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 {
|
||||
text: Default::default(),
|
||||
text: "Hello, world. This is a test of the current text-to-speech values.".into(),
|
||||
tts,
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +49,13 @@ fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
|
|||
let volume = volume.parse::<f32>().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 => {
|
||||
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> {
|
||||
let should_show_voices = model.tts.voices().unwrap().iter().len() > 0;
|
||||
form![
|
||||
div![label![
|
||||
"Text to speak",
|
||||
|
@ -96,6 +111,36 @@ fn view(model: &Model) -> Node<Msg> {
|
|||
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![
|
||||
"Speak",
|
||||
ev(Ev::Click, |e| {
|
||||
|
@ -107,5 +152,6 @@ fn view(model: &Model) -> Node<Msg> {
|
|||
}
|
||||
|
||||
fn main() {
|
||||
console_log::init().expect("Error initializing logger");
|
||||
App::start("app", init, update, view);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
#[cfg(target_arch = "wasm32")]
|
||||
use std::sync::Mutex;
|
||||
use std::{str::FromStr, sync::Mutex};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use log::{info, trace};
|
||||
use unic_langid::LanguageIdentifier;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{
|
||||
SpeechSynthesisErrorCode, SpeechSynthesisErrorEvent, SpeechSynthesisEvent,
|
||||
SpeechSynthesisUtterance,
|
||||
SpeechSynthesisUtterance, SpeechSynthesisVoice,
|
||||
};
|
||||
|
||||
use crate::Gender;
|
||||
use crate::Voice;
|
||||
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -18,6 +21,7 @@ pub struct Web {
|
|||
rate: f32,
|
||||
pitch: f32,
|
||||
volume: f32,
|
||||
voice: Option<SpeechSynthesisVoice>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
@ -35,6 +39,7 @@ impl Web {
|
|||
rate: 1.,
|
||||
pitch: 1.,
|
||||
volume: 1.,
|
||||
voice: None,
|
||||
};
|
||||
*backend_id += 1;
|
||||
Ok(rv)
|
||||
|
@ -53,7 +58,8 @@ impl Backend for Web {
|
|||
pitch: true,
|
||||
volume: true,
|
||||
is_speaking: true,
|
||||
voices: true,
|
||||
voice: true,
|
||||
get_voice: true,
|
||||
utterance_callbacks: true,
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +70,9 @@ impl Backend for Web {
|
|||
utterance.set_rate(self.rate);
|
||||
utterance.set_pitch(self.pitch);
|
||||
utterance.set_volume(self.volume);
|
||||
if self.voice.is_some() {
|
||||
utterance.set_voice(self.voice.as_ref());
|
||||
}
|
||||
let id = self.id().unwrap();
|
||||
let mut uid = NEXT_UTTERANCE_ID.lock().unwrap();
|
||||
let utterance_id = UtteranceId::Web(*uid);
|
||||
|
@ -198,16 +207,53 @@ impl Backend for Web {
|
|||
}
|
||||
}
|
||||
|
||||
fn voice(&self) -> Result<String, Error> {
|
||||
unimplemented!()
|
||||
fn voice(&self) -> Result<Option<Voice>, Error> {
|
||||
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> {
|
||||
unimplemented!()
|
||||
fn voices(&self) -> Result<Vec<Voice>, Error> {
|
||||
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> {
|
||||
unimplemented!()
|
||||
fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 is_speaking(&self) -> Result<bool, 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>;
|
||||
}
|
||||
|
||||
|
@ -582,7 +582,7 @@ impl Tts {
|
|||
/**
|
||||
* 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();
|
||||
if get_voice {
|
||||
self.0.read().unwrap().voice()
|
||||
|
@ -702,7 +702,7 @@ impl Drop for Tts {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Gender {
|
||||
Unspecified,
|
||||
Male,
|
||||
|
@ -711,8 +711,26 @@ pub enum Gender {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct Voice {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub gender: Gender,
|
||||
pub language: LanguageIdentifier,
|
||||
pub(crate) id: String,
|
||||
pub(crate) name: String,
|
||||
pub(crate) gender: Gender,
|
||||
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
Block a user