From 00ba1948b22252ae8d6c4c8a7237047842167040 Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 18 Aug 2026 09:35:49 +0000 Subject: [PATCH 1/2] =?UTF-8?q?release=20v0.3.20=20=E2=80=94=20speechmarkd?= =?UTF-8?q?own-rust=200.4.13=20(Azure=20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 36ca65f..352a899 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-tts-wrapper" -version = "0.3.19" +version = "0.3.20" edition = "2021" license = "MIT" description = "Cross-platform TTS wrapper with C API — mirrors js-tts-wrapper / SwiftTTSWrapper" @@ -61,7 +61,7 @@ serde_json = { version = "1", optional = true } # symbols an older prebuilt lib lacks → LNK2019). Bump both together. sherpa-onnx = { version = "=1.13.5", optional = true } base64 = { version = "0.22", optional = true } -speechmarkdown-rust = { version = "0.4.12", optional = true } +speechmarkdown-rust = { version = "0.4.13", optional = true } anyhow = "1" tungstenite = { version = "0.29.0", features = ["rustls-tls-webpki-roots"], optional = true } uuid = { version = "1.23.2", features = ["v4"], optional = true } From 4beb61bebd75fbd01a51650ba2aace9eb9a71b1f Mon Sep 17 00:00:00 2001 From: will wade Date: Tue, 18 Aug 2026 09:42:44 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(cloud):=20Edge=20also=20zero-audios=20m?= =?UTF-8?q?stts:express-as=20=E2=80=94=20strip=20style=20tags,=20keep=20te?= =?UTF-8?q?xt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live testing of SpeechMarkdown #[style] sections through Edge (which #24 routed to the Azure platform, so they now emit ) showed the free Edge endpoint synthesises zero audio for express-as just like . The zero-audio Err from #23 surfaced it loudly instead of silently. In Edge mode the strip now drops the express-as wrapper tags and keeps the spoken content. Azure keeps express-as (documented, accepted). --- examples/edge-style.rs | 24 ++++++++++++++++++ src/cloud_engine.rs | 56 +++++++++++++++++++++++++++++------------- 2 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 examples/edge-style.rs diff --git a/examples/edge-style.rs b/examples/edge-style.rs new file mode 100644 index 0000000..82d8ec1 --- /dev/null +++ b/examples/edge-style.rs @@ -0,0 +1,24 @@ +//! Live check: `SpeechMarkdown` style sections through Edge (mstts:express-as +//! is zero-audio on the free endpoint, so the Edge path strips the tags and +//! keeps the text). +use rust_tts_wrapper::factory::create_engine; +fn main() { + let engine = create_engine("edge", "{}").expect("edge engine"); + for text in ["Plain control.", "#[angry] I am angry!"] { + let mut bytes = 0usize; + engine + .speak( + text, + Some("en-GB-SoniaNeural"), + 1.0, + 1.0, + 1.0, + Some(&mut |c: &[u8]| bytes += c.len()), + None, + ) + .unwrap_or_else(|e| panic!("{text}: {e}")); + println!("{text:?}: {bytes} bytes"); + assert!(bytes > 0, "{text}: no audio"); + } + println!("PASS"); +} diff --git a/src/cloud_engine.rs b/src/cloud_engine.rs index c0890e1..4cdb366 100644 --- a/src/cloud_engine.rs +++ b/src/cloud_engine.rs @@ -1193,25 +1193,30 @@ fn normalize_ssml_envelope(ssml: &str, voice: &str) -> String { format!("{lead}{open}{}", &trimmed[tag_end + 1..]) } -/// Remove unsupported position-marking elements from an SSML document -/// for Azure/Edge. +/// Remove elements the target endpoint silently refuses, from an SSML +/// document bound for Azure/Edge. /// -/// Neither service supports the W3C SSML `` element — an utterance -/// containing one synthesises **zero audio**, again with no error from -/// the service. Azure proper documents its own `` +/// Both services lack the W3C SSML `` element — an utterance +/// containing one synthesises **zero audio**, with no error from the +/// service. Azure proper documents its own `` /// replacement element, but the **free Edge endpoint zero-audios on -/// `` too** (verified live), so Edge strips both while Azure -/// keeps bookmarks. Both are empty elements (they only name a position), -/// so dropping them changes no spoken content; consumers that need the -/// positions should use word-boundary events. speech-dispatcher's -/// wrapper injects `` around every pause, so -/// pass-through SSML from SSIP clients hits this constantly. +/// `` and on `` style sections too** +/// (verified live), so in Edge mode (`is_edge`) those tags are dropped +/// as well. ``/`` are empty elements, and for the +/// paired `mstts:express-as` wrapper only the tags are removed — the +/// spoken content inside survives. Consumers that need positions should +/// use word-boundary events. speech-dispatcher's wrapper injects +/// `` around every pause, so pass-through SSML +/// from SSIP clients hits this constantly. #[cfg(feature = "cloud")] -fn strip_unsupported_marks(ssml: &str, strip_bookmark: bool) -> String { +fn strip_unsupported_marks(ssml: &str, is_edge: bool) -> String { let has_marks = ssml.contains(" String { }; let drop = name_done(after, "') { out.push_str(&rest[..pos]); @@ -3618,6 +3626,20 @@ mod tests { ); } + #[test] + fn test_express_as_kept_for_azure_tags_dropped_for_edge() { + // SpeechMarkdown #[style] sections become mstts:express-as wrappers. + let styled = "I am angry!"; + assert_eq!(strip_unsupported_marks(styled, false), styled); + // Edge zero-audios on express-as; the tags go, the spoken text stays. + assert_eq!(strip_unsupported_marks(styled, true), "I am angry!"); + // Sibling mstts elements are not touched. + assert_eq!( + strip_unsupported_marks("", true), + "" + ); + } + // ===== inject_voice_if_missing ===== #[test]