fix(cloud): azure/edge silent zero-audio — bare <speak> envelopes, <mark>, and zero-audio-as-success - #23
Merged
Conversation
…nd <mark>
Two Azure/Edge failure modes complete the turn normally (turn.end /
HTTP 200) but synthesise zero audio, with no error anywhere —
consumers saw a silent success:
- A bare <speak> envelope (no version/xmlns/xml:lang) — exactly what
speech-dispatcher's index-marking wrapper sends. normalize_ssml_envelope()
now completes the envelope before the request goes out (xml:lang
derived from the voice name, like build_azure_ssml).
- An SSML <mark> element, which Azure/Edge don't support —
speech-dispatcher injects <mark name="__spd_N"/> around every pause.
strip_unsupported_marks() drops the (empty, unspoken) elements.
Defense in depth: a turn that finishes cleanly with zero audio now
returns Err("synthesis completed with no audio") on both the WS path
(azure/edge) and every REST response path, so any future silent
zero-audio failure surfaces as an error.
Verified against the live Edge endpoint: plain text, bare <speak>,
and <mark> documents all synthesise (examples/edge-bare-envelope.rs).
This was referenced Aug 18, 2026
Merged
Merged
willwade
added a commit
that referenced
this pull request
Aug 18, 2026
…, keep text Live testing of SpeechMarkdown #[style] sections through Edge (which #24 routed to the Azure platform, so they now emit <mstts:express-as style=…>) showed the free Edge endpoint synthesises zero audio for express-as just like <bookmark>. 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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found while debugging AACTools/VoiceGarden-SPD#1 (speech-dispatcher SSML produced no audio, reported as success). Two Azure/Edge failure modes complete the turn normally (
turn.end/ HTTP 200) but synthesise zero audio, with no error anywhere:1. Bare
<speak>envelope → zero audioAzure/Edge accept an envelope missing
version/xmlns/xml:lang— the turn completes cleanly and nothing is synthesised.xml:langis the required missing piece. That is exactly what speech-dispatcher's index-marking wrapper sends, so every SSIP client using SSML mode hits this.Fix:
normalize_ssml_envelope()completes the envelope before the request goes out (WS + REST paths), derivingxml:langfrom the voice name (en-GB-SoniaNeural→en-GB), same heuristic asbuild_azure_ssml. Present attributes, custom namespaces, tag-name case and the rest of the document pass through verbatim; plain text and envelope-less fragments are untouched.2. SSML
<mark>element → zero audioAzure/Edge don't support
<mark>; one anywhere in the document silently zeroes the whole utterance. speech-dispatcher injects<mark name="__spd_N"/>around every pause, so this is the same repro from the other direction.Fix:
strip_unsupported_marks()drops the (empty, unspoken)<mark>/</mark>tags on the azure/edge SSML path. Consumers needing positions should use word-boundary events, which already exist.3. Zero audio reported as
Ok(())(all cloud engines)The wrapper was the only layer that knew a turn produced nothing, yet returned success — every consumer had to detect "no bytes" independently. Now:
turn.endwith zero bytes →Err("… synthesis completed with no audio").audioContent, ElevenLabsaudio_base64, raw body): decoded/delivered bytes counted; zero →Err("… synthesis returned no audio").This is what surfaced failure #2 during verification — the error path works.
Verification
inject_voice_if_missing),voice_lang, mark stripping (speechd shape, paired tags,<market>untouched, unterminated left alone).cargo run --example edge-bare-envelope: plain text, bare<speak>, and<speak>+<mark>all synthesise audio with word boundaries (previously: 0 bytes,Ok(())).cargo fmt,cargo clippy -D warnings(cloud and system,cloud feature sets), full test suite green.Consumers: VoiceGarden-SPD currently works around #1/#2 module-side (AACTools/VoiceGarden-SPD#3); once this merges, the module's
ensure_ssml_documentbecomes a harmless no-op and can be simplified.