Add voices api.

Get voice locales known to speech-dispatcher.
Get possible voice list for a given locale.
Added getting and setting the user's chosen voice.
Tell orca to reload settings after changing.
Initialize user's voice from orca settings.
This commit is contained in:
Jeremy Whiting 2025-06-05 12:00:34 -06:00 committed by Jeremy Whiting
parent 36685ce630
commit 02a15c9295
5 changed files with 332 additions and 2 deletions

View file

@ -12,8 +12,13 @@ use input_linux::Key;
use lazy_static::lazy_static;
use nix::sys::signal;
use nix::unistd::Pid;
#[cfg(not(test))]
use nix::unistd::{Uid, User};
use num_enum::TryFromPrimitive;
use serde_json::{Map, Value};
use speech_dispatcher::Voice;
#[cfg(not(test))]
use speech_dispatcher::{Connection as SDConnection, Mode};
use std::collections::HashMap;
use std::io::ErrorKind;
use std::ops::RangeInclusive;
@ -40,6 +45,8 @@ const ORCA_SETTINGS: &str = "orca/user-settings.conf";
const PITCH_SETTING: &str = "average-pitch";
const RATE_SETTING: &str = "rate";
const VOLUME_SETTING: &str = "gain";
const FAMILY_SETTING: &str = "family";
const VOICE_NAME_SETTING: &str = "name";
const ENABLE_SETTING: &str = "enableSpeech";
const A11Y_SETTING: &str = "org.gnome.desktop.a11y.applications";
@ -49,6 +56,7 @@ const KEYBOARD_NAME: &str = "steamos-manager";
const PITCH_DEFAULT: f64 = 5.0;
const RATE_DEFAULT: f64 = 50.0;
const VOLUME_DEFAULT: f64 = 10.0;
const VOICE_NAME_DEFAULT: &str = "default";
lazy_static! {
static ref VALID_SETTINGS: HashMap<&'static str, RangeInclusive<f64>> = HashMap::from_iter([
@ -89,7 +97,10 @@ pub(crate) struct OrcaManager<'dbus> {
volume: f64,
enabled: bool,
mode: ScreenReaderMode,
voice: String,
keyboard: UInputDevice,
voices: HashMap<String, Voice>,
voices_by_language: HashMap<String, Vec<String>>,
}
impl<'dbus> OrcaManager<'dbus> {
@ -102,7 +113,10 @@ impl<'dbus> OrcaManager<'dbus> {
enabled: true,
// Always start in browse mode for now, since we have no storage to remember this property
mode: ScreenReaderMode::Browse,
voice: String::new(),
keyboard: UInputDevice::new()?,
voices: HashMap::new(),
voices_by_language: HashMap::new(),
};
let _ = manager
.load_values()
@ -124,9 +138,46 @@ impl<'dbus> OrcaManager<'dbus> {
Key::Up,
])?;
#[cfg(not(test))]
match manager.init_voice_list() {
Ok(()) => trace!("Voice list loaded"),
Err(e) => error!("Unable to init voice list. {e}"),
}
Ok(manager)
}
#[cfg(not(test))]
fn init_voice_list(&mut self) -> Result<()> {
const CLIENT_NAME: &str = "steamos-manager";
const CONNECTION_NAME: &str = "steamos-manager";
let user_name = User::from_uid(Uid::current())?
.ok_or(anyhow!("Unable to get current user"))?
.name;
let connection =
SDConnection::open(CLIENT_NAME, CONNECTION_NAME, &user_name, Mode::Threaded)?;
let voices = connection.list_synthesis_voices()?.to_vec();
for v in voices.iter() {
let name = &v.name;
let lang = &v.language;
self.voices.insert(name.clone(), v.clone());
self.voices_by_language
.entry(lang.clone())
.or_default()
.push(name.clone());
}
Ok(())
}
pub fn get_voices(&self, locale: &str) -> Option<Vec<String>> {
self.voices_by_language.get(locale).cloned()
}
pub fn get_voice_locales(&self) -> Vec<String> {
self.voices_by_language.keys().cloned().collect()
}
#[cfg(not(test))]
fn settings_path(&self) -> Result<PathBuf> {
let xdg_base = BaseDirectories::new();
@ -170,6 +221,21 @@ impl<'dbus> OrcaManager<'dbus> {
Ok(())
}
pub fn voice(&self) -> String {
self.voice.clone()
}
pub async fn set_voice(&mut self, voice: &str) -> Result<()> {
let properties = self
.voices
.get(voice)
.ok_or(anyhow!("Invalid voice specified"))?;
self.set_orca_voice(properties).await?;
self.voice = voice.to_string();
self.reload_orca().await?;
Ok(())
}
pub fn pitch(&self) -> f64 {
self.pitch
}
@ -398,9 +464,83 @@ impl<'dbus> OrcaManager<'dbus> {
self.rate, self.pitch, self.volume
);
if let Some(family) = default_voice.get(FAMILY_SETTING) {
if let Some(name) = family.get(VOICE_NAME_SETTING) {
self.voice = name
.as_str()
.unwrap_or_else(|| {
error!("Unable to convert orca default voice family name to string value");
VOICE_NAME_DEFAULT
})
.to_string();
} else {
warn!("Unable to load default voice family name from orca user-settings.conf");
}
} else {
warn!("Unable to load default voice family from orca user-settings.conf");
}
Ok(())
}
async fn set_orca_voice(&self, voice: &Voice) -> Result<()> {
let data = read_to_string(self.settings_path()?).await?;
let mut json: Value = serde_json::from_str(&data)?;
let profiles = json
.as_object_mut()
.ok_or(anyhow!("orca user-settings.conf json is not an object"))?
.entry("profiles")
.or_insert(Value::Object(Map::new()));
let default_profile = profiles
.as_object_mut()
.ok_or(anyhow!("orca user-settings.conf profiles is not an object"))?
.entry("default")
.or_insert(Value::Object(Map::new()));
let voices = default_profile
.as_object_mut()
.ok_or(anyhow!(
"orca user-settings.conf default profile is not an object"
))?
.entry("voices")
.or_insert(Value::Object(Map::new()));
let default_voice = voices
.as_object_mut()
.ok_or(anyhow!("orca user-settings.conf voices is not an object"))?
.entry("default")
.or_insert(Value::Object(Map::new()));
let family = default_voice
.as_object_mut()
.ok_or(anyhow!("orca user-settings.conf family is not an object"))?
.entry("family")
.or_insert(Value::Object(Map::new()));
// If we have a dialect use it, otherwise leave it blank
let mut language = voice.language.clone();
let mut dialect = "";
if let Some((l, d)) = voice.language.split_once("-") {
language = l.to_string();
dialect = d;
}
let mut_family = family.as_object_mut().ok_or(anyhow!(
"orca user-settings.conf default voice family is not an object"
))?;
mut_family.insert("name".to_string(), voice.name.clone().into());
mut_family.insert("lang".to_string(), language.into());
mut_family.insert("variant".to_string(), voice.variant.clone().into());
mut_family.insert("dialect".to_string(), dialect.into());
// Set established property
default_voice
.as_object_mut()
.ok_or(anyhow!(
"orca user-settings.conf default voice is not an object"
))?
.insert("established".to_string(), true.into());
let data = serde_json::to_string_pretty(&json)?;
Ok(write(self.settings_path()?, data.as_bytes()).await?)
}
async fn set_orca_option(&self, option: &str, value: f64) -> Result<()> {
if let Some(range) = VALID_SETTINGS.get(option) {
ensure!(