Add experimental screen_reader feature

Part of https://github.com/emilk/egui/issues/167
This commit is contained in:
Emil Ernerfeldt 2021-03-08 20:58:01 +01:00
parent cb7ef6faeb
commit 44cd304cdf
13 changed files with 504 additions and 3 deletions

318
Cargo.lock generated
View file

@ -66,6 +66,15 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000444226fcff248f2bc4c7625be32c63caccfecc2723a2b9f78a7487a49c407"
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "atomic_refcell"
version = "0.1.6"
@ -119,6 +128,29 @@ dependencies = [
"serde",
]
[[package]]
name = "bindgen"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd4865004a46a0aafb2a0a5eb19d3c9fc46ee5f063a6cfc605c69ac9ecf5263d"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"clap",
"env_logger",
"lazy_static",
"lazycell",
"log",
"peeking_take_while",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"which",
]
[[package]]
name = "bit-set"
version = "0.5.2"
@ -176,6 +208,12 @@ version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]]
name = "bytes"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
[[package]]
name = "calloop"
version = "0.6.5"
@ -201,6 +239,21 @@ version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27"
dependencies = [
"nom 5.1.2",
]
[[package]]
name = "cfg-if"
version = "0.1.10"
@ -241,15 +294,30 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "clang-sys"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f54d78e30b388d4815220c8dd03fea5656b6c6d32adb59e89061552a102f8da1"
dependencies = [
"glob",
"libc",
"libloading 0.7.0",
]
[[package]]
name = "clap"
version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim 0.8.0",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
@ -327,6 +395,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "combine"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4369b5e4c0cddf64ad8981c0111e7df4f7078f4d6ba98fb31f2e17c4c57b7e"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "core-foundation"
version = "0.7.0"
@ -541,7 +619,7 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"strsim 0.9.3",
"syn",
]
@ -628,6 +706,33 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
[[package]]
name = "dyn-clonable"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e9232f0e607a262ceb9bd5141a3dfb3e4db6994b31989bbfd845878cba59fd4"
dependencies = [
"dyn-clonable-impl",
"dyn-clone",
]
[[package]]
name = "dyn-clonable-impl"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "558e40ea573c374cf53507fd240b7ee2f5477df7cfebdb97323ec61c719399c5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dyn-clone"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf"
[[package]]
name = "eframe"
version = "0.10.0"
@ -678,6 +783,7 @@ dependencies = [
"glium",
"serde",
"serde_json",
"tts",
"ureq",
"webbrowser",
]
@ -691,6 +797,7 @@ dependencies = [
"js-sys",
"serde",
"serde_json",
"tts",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@ -709,6 +816,19 @@ dependencies = [
"serde",
]
[[package]]
name = "env_logger"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]]
name = "epaint"
version = "0.10.0"
@ -799,6 +919,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
[[package]]
name = "gcc"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
[[package]]
name = "getrandom"
version = "0.2.2"
@ -843,6 +969,12 @@ dependencies = [
"takeable-option",
]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "glutin"
version = "0.26.0"
@ -936,6 +1068,12 @@ dependencies = [
"libc",
]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "ident_case"
version = "1.0.1"
@ -1021,6 +1159,20 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "jni"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf"
dependencies = [
"cesu8",
"combine",
"jni-sys",
"log",
"thiserror",
"walkdir",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
@ -1316,6 +1468,16 @@ dependencies = [
"libc",
]
[[package]]
name = "nom"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
dependencies = [
"memchr",
"version_check",
]
[[package]]
name = "nom"
version = "6.1.2"
@ -1489,6 +1651,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "percent-encoding"
version = "2.1.0"
@ -1683,6 +1851,12 @@ version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.2.3"
@ -1814,6 +1988,12 @@ dependencies = [
"serde",
]
[[package]]
name = "sha1"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
[[package]]
name = "shared_library"
version = "0.1.9"
@ -1824,6 +2004,12 @@ dependencies = [
"libc",
]
[[package]]
name = "shlex"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2"
[[package]]
name = "slab"
version = "0.4.2"
@ -1855,12 +2041,38 @@ dependencies = [
"wayland-protocols",
]
[[package]]
name = "speech-dispatcher"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "076efad2c03a1c5cc41eaa82c30770130e014f6544769edea79e096a2f134412"
dependencies = [
"lazy_static",
"speech-dispatcher-sys",
]
[[package]]
name = "speech-dispatcher-sys"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b662a91fe7e39d3d11edcf0297c717a8c05683a6c6445df9aba83b034b2b2db5"
dependencies = [
"bindgen",
"gcc",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "strsim"
version = "0.9.3"
@ -1906,6 +2118,15 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36ae8932fcfea38b7d3883ae2ab357b0d57a02caaa18ebb4f5ece08beaec4aa0"
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.11.0"
@ -1994,6 +2215,37 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc"
[[package]]
name = "tts"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "592f268a2431704677a2cfa2d712dd0518b99bd1630f5977423e76db1cc826d2"
dependencies = [
"cocoa-foundation",
"dyn-clonable",
"jni",
"lazy_static",
"libc",
"log",
"ndk-glue",
"objc",
"speech-dispatcher",
"thiserror",
"tts_winrt_bindings",
"wasm-bindgen",
"web-sys",
"winrt",
]
[[package]]
name = "tts_winrt_bindings"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cf13b200c067076f7a6001b3eac9a97d4fa0a322b10a2f958fd1176a1e1461"
dependencies = [
"winrt",
]
[[package]]
name = "unicode-bidi"
version = "0.3.4"
@ -2058,6 +2310,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.2"
@ -2270,6 +2528,15 @@ dependencies = [
"webpki",
]
[[package]]
name = "which"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724"
dependencies = [
"libc",
]
[[package]]
name = "widestring"
version = "0.4.3"
@ -2350,6 +2617,53 @@ dependencies = [
"x11-dl",
]
[[package]]
name = "winrt"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf62b1e56a9efd6caec59a053450dd9fe5bc3d0dc28a27d3054b85b68db95a4"
dependencies = [
"sha1",
"winrt_macros",
]
[[package]]
name = "winrt_gen"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497492d5f30d47cb397c3d5bee37700996822311062a3a609493523ad3bb9ae"
dependencies = [
"proc-macro2",
"quote",
"serde_json",
"sha1",
"syn",
"winrt_gen_macros",
]
[[package]]
name = "winrt_gen_macros"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cd78ebb3f0f35f3d9b7f3272b7afac1904cc3e3f636a0a6e5877d72c78b7d8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "winrt_macros"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d88594f878eee84909a5d20cad8bfef8587f9e1473ce81b74fbbd4b12748d662"
dependencies = [
"proc-macro2",
"quote",
"syn",
"winrt_gen",
]
[[package]]
name = "ws2_32-sys"
version = "0.2.1"
@ -2397,7 +2711,7 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a9a231574ae78801646617cefd13bfe94be907c0e4fa979cfd8b770aa3c5d08"
dependencies = [
"nom",
"nom 6.1.2",
]
[[package]]

View file

@ -13,7 +13,17 @@ rm -f docs/${CRATE_NAME}_bg.wasm
echo "Building rust…"
BUILD=release
cargo build --release --all-features -p ${CRATE_NAME} --lib --target wasm32-unknown-unknown
FEATURES="http,persistence" # screen_reader is experimental
# FEATURES="http,persistence,screen_reader" # screen_reader is experimental
(
cd egui_demo_app && cargo build \
-p ${CRATE_NAME} \
--release \
--lib \
--target wasm32-unknown-unknown \
--features ${FEATURES}
)
echo "Generating JS bindings for wasm…"
TARGET_NAME="${CRATE_NAME}.wasm"

View file

@ -30,4 +30,5 @@ egui_web = { version = "0.10.0", path = "../egui_web" }
default = []
http = ["egui_glium/http", "egui_web/http"]
persistence = ["epi/persistence", "egui_glium/persistence", "egui_web/persistence"]
screen_reader = ["egui_glium/screen_reader", "egui_web/screen_reader"] # experimental
time = ["egui_glium/time"] # for seconds_since_midnight

View file

@ -31,6 +31,19 @@ impl Output {
pub fn open_url(&mut self, url: impl Into<String>) {
self.open_url = Some(OpenUrl::same_tab(url))
}
/// This can be used by a text-to-speech system to describe the events (if any).
pub fn events_description(&self) -> String {
// only describe last event:
for event in self.events.iter().rev() {
match event {
OutputEvent::WidgetEvent(WidgetEvent::Focus, widget_info) => {
return widget_info.description();
}
}
}
Default::default()
}
}
#[derive(Clone, PartialEq)]
@ -202,6 +215,63 @@ impl WidgetInfo {
..Self::new(WidgetType::TextEdit)
}
}
/// This can be used by a text-to-speech system to describe the widget.
pub fn description(&self) -> String {
let Self {
typ,
label,
edit_text,
selected,
value,
} = self;
// TODO: localization
let widget_name = match typ {
WidgetType::Label => "",
WidgetType::Hyperlink => "link",
WidgetType::TextEdit => "text edit",
WidgetType::Button => "button",
WidgetType::Checkbox => "checkbox",
WidgetType::RadioButton => "radio",
WidgetType::SelectableLabel => "selectable",
WidgetType::ComboBox => "combo",
WidgetType::Slider => "slider",
WidgetType::DragValue => "drag value",
WidgetType::ColorButton => "color button",
WidgetType::ImageButton => "image button",
WidgetType::CollapsingHeader => "collapsing header",
WidgetType::Other => "",
};
let mut description = widget_name.to_owned();
if let Some(selected) = selected {
if *typ == WidgetType::Checkbox {
description += " ";
description += if *selected { "checked" } else { "unchecked" };
} else {
description += if *selected { "selected" } else { "" };
};
}
if let Some(label) = label {
description += " ";
description += label;
}
if let Some(edit_text) = edit_text {
description += " ";
description += edit_text;
}
if let Some(value) = value {
description += " ";
description += &value.to_string();
}
description.trim().to_owned()
}
}
/// The different types of built-in widgets in egui

View file

@ -17,4 +17,5 @@ egui_demo_lib = { version = "0.10.0", path = "../egui_demo_lib" }
default = ["persistence"]
http = ["eframe/http", "egui_demo_lib/http"]
persistence = ["eframe/persistence", "egui_demo_lib/persistence"]
screen_reader = ["eframe/screen_reader"] # experimental
syntect = ["egui_demo_lib/syntect"]

View file

@ -31,6 +31,9 @@ directories-next = { version = "2", optional = true }
serde = { version = "1", optional = true }
serde_json = { version = "1", optional = true }
# feature screen_reader
tts = { version = "0.14", optional = true }
# feature "time"
chrono = { version = "0.4", optional = true }
@ -46,3 +49,4 @@ persistence = [
"serde",
]
time = ["chrono"] # for seconds_since_midnight
screen_reader = ["tts"] # experimental

View file

@ -180,6 +180,8 @@ pub fn run(mut app: Box<dyn epi::App>) -> ! {
#[cfg(feature = "http")]
let http = std::sync::Arc::new(crate::http::GliumHttp {});
let mut screen_reader = crate::screen_reader::ScreenReader::default();
if app.warm_up_enabled() {
// let warm_up_start = Instant::now();
input_state.raw.time = Some(0.0);
@ -273,6 +275,7 @@ pub fn run(mut app: Box<dyn epi::App>) -> ! {
};
}
screen_reader.speak(&egui_output.events_description());
handle_output(egui_output, &display, clipboard.as_mut());
#[cfg(feature = "persistence")]

View file

@ -15,6 +15,7 @@ pub mod http;
mod painter;
#[cfg(feature = "persistence")]
pub mod persistence;
pub mod screen_reader;
pub mod window_settings;
pub use backend::*;

View file

@ -0,0 +1,44 @@
pub struct ScreenReader {
#[cfg(feature = "screen_reader")]
tts: Option<tts::TTS>,
}
#[cfg(not(feature = "screen_reader"))]
impl Default for ScreenReader {
fn default() -> Self {
Self {}
}
}
#[cfg(feature = "screen_reader")]
impl Default for ScreenReader {
fn default() -> Self {
let tts = match tts::TTS::default() {
Ok(screen_reader) => {
eprintln!("Initialized screen reader.");
Some(screen_reader)
}
Err(err) => {
eprintln!("Failed to load screen reader: {}", err);
None
}
};
Self { tts }
}
}
impl ScreenReader {
#[cfg(not(feature = "screen_reader"))]
pub fn speak(&mut self, _text: &str) {}
#[cfg(feature = "screen_reader")]
pub fn speak(&mut self, text: &str) {
if let Some(tts) = &mut self.tts {
eprintln!("Speaking: {:?}", text);
let interrupt = true;
if let Err(err) = tts.speak(text, interrupt) {
eprintln!("Failed to read: {}", err);
}
}
}
}

View file

@ -25,6 +25,7 @@ epi = { version = "0.10.0", path = "../epi" }
js-sys = "0.3"
serde = { version = "1", optional = true }
serde_json = { version = "1", optional = true }
tts = { version = "0.14", optional = true } # feature screen_reader
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
@ -39,6 +40,7 @@ http = [
"web-sys/Response",
]
persistence = ["serde", "serde_json"]
screen_reader = ["tts"] # experimental
[dependencies.web-sys]
version = "0.3"

View file

@ -135,6 +135,7 @@ pub struct AppRunner {
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
storage: LocalStorage,
last_save_time: f64,
screen_reader: crate::screen_reader::ScreenReader,
#[cfg(feature = "http")]
http: Arc<http::WebHttp>,
}
@ -152,6 +153,7 @@ impl AppRunner {
needs_repaint: Default::default(),
storage,
last_save_time: now_sec(),
screen_reader: Default::default(),
#[cfg(feature = "http")]
http: Arc::new(http::WebHttp {}),
})
@ -217,6 +219,7 @@ impl AppRunner {
let egui_ctx = &self.web_backend.ctx;
self.app.update(egui_ctx, &mut frame);
let (egui_output, clipped_meshes) = self.web_backend.end_frame()?;
self.screen_reader.speak(&egui_output.events_description());
handle_output(&egui_output);
{

View file

@ -12,6 +12,7 @@ pub mod backend;
#[cfg(feature = "http")]
pub mod http;
mod painter;
pub mod screen_reader;
pub mod webgl1;
pub mod webgl2;

View file

@ -0,0 +1,47 @@
pub struct ScreenReader {
#[cfg(feature = "screen_reader")]
tts: Option<tts::TTS>,
}
#[cfg(not(feature = "screen_reader"))]
impl Default for ScreenReader {
fn default() -> Self {
Self {}
}
}
#[cfg(feature = "screen_reader")]
impl Default for ScreenReader {
fn default() -> Self {
let tts = match tts::TTS::default() {
Ok(screen_reader) => {
crate::console_log("Initialized screen reader.");
Some(screen_reader)
}
Err(err) => {
crate::console_warn(format!("Failed to load screen reader: {}", err));
None
}
};
Self { tts }
}
}
impl ScreenReader {
#[cfg(not(feature = "screen_reader"))]
pub fn speak(&mut self, _text: &str) {}
#[cfg(feature = "screen_reader")]
pub fn speak(&mut self, text: &str) {
if text.is_empty() {
return;
}
if let Some(tts) = &mut self.tts {
crate::console_log(format!("Speaking: {:?}", text));
let interrupt = true;
if let Err(err) = tts.speak(text, interrupt) {
crate::console_warn(format!("Failed to read: {}", err));
}
}
}
}