SSML input¶
Drive lip-sync from the same SSML you feed your TTS (issue #52). Game and VO
pipelines already author their TTS input as SSML
— the W3C markup Azure, Google and Amazon Polly consume — and OpenFaceFX's
text tags were modelled on it. parse_ssml is a thin front-end
over those #7 tags, not a new animation path: it parses an SSML document with
the stdlib xml.etree.ElementTree and returns the same (clean_text, tags)
that parse_tagged_transcript yields, so the unchanged naive pipeline lip-syncs
it identically — breaks land as pauses, emphasis as stronger articulation, marks
as events.
Enable it with the naive --ssml flag, or just pass a document with a <speak>
root (auto-detected):
python -m openfacefx naive --ssml --duration 3 -o out.track.json \
--text '<speak>Say <emphasis level="strong">brave</emphasis> <break time="300ms"/> new world <mark name="beat"/></speak>'
Each construct maps onto an existing text-tag primitive — nothing new is invented:
| SSML | Maps to | Notes |
|---|---|---|
<break time="500ms"/> / strength |
[pause:S] |
time (ms/s) wins over strength; the strength table defaults a bare <break/> to medium (0.5 s) |
<emphasis level="strong">…</emphasis> |
[emphasis strength=..] |
level → dominance strength (strong 1.0, moderate 0.5, reduced/none 0); a level-less <emphasis> uses the #7 default |
<sub alias="World Health Organization">WHO</sub> |
substituted text | the spoken alias replaces the written form for G2P |
<mark name="hit"/> |
[mark name=hit] |
a named phrase marker event |
<p> / <s> |
[phrase] |
a phrase boundary at the element start |
<say-as interpret-as=…>…</say-as> |
qa.normalize_transcript |
the enclosed text is folded to ASCII (the #23 pass) |
Deferred / out of scope: <phoneme alphabet="ipa" ph="…"> pronunciation
override belongs to the multi-language framework (#8) + ipa.py — its text is
passed through unchanged here, the ph ignored. Any unknown element degrades
to its text content and never crashes; malformed XML raises a clear
ValueError at the boundary (a DOCTYPE is rejected outright — SSML has none,
and it can smuggle entity-expansion).
Because it produces the same (clean_text, tags) pair as the bracket front-end,
an SSML document is byte-identical to the equivalent tagged transcript
through the whole pipeline, and a <speak> with no constructs is
byte-identical to plain naive --text. The parse is deterministic and
stdlib-only (xml.etree / re); numpy is never imported. Library callers get
parse_ssml, looks_like_ssml, and the extensible EMPHASIS_STRENGTH /
BREAK_STRENGTH tables.
openfacefx.ssml
¶
SSML input adapter (issue #52): drive lip-sync from the same SSML you feed TTS.
A thin front-end over the issue-#7 text-tag system -- not a new animation
path. Game / VO pipelines already author their TTS input as SSML (the W3C Speech
Synthesis Markup Language that Azure, Google and Amazon Polly consume), and
OpenFaceFX's text tags were deliberately modelled on it. :func:parse_ssml
parses an SSML document with the stdlib xml.etree.ElementTree and returns the
same (clean_text, List[Tag]) the bracket-tag front-end
(:func:openfacefx.texttags.parse_tagged_transcript) yields, so the unchanged
naive pipeline lip-syncs it identically. Every supported construct maps onto an
existing #7 primitive -- nothing new is invented:
<break time="500ms"/>/strength-> apause:class:~openfacefx. texttags.Tag(the[pause:S]path);time(ms/s) wins overstrength.<emphasis level="strong">...</emphasis>-> anemphasisTag,levelmapped to an issue-#18 dominancestrength(see :data:EMPHASIS_STRENGTH); a level-less<emphasis>carries no strength, so the #7 default applies.<sub alias="World Health Organization">WHO</sub>-> the spokenaliasis substituted intoclean_textfor G2P.<mark name="hit"/>-> a namedphrase(marker) Tag;<p>/<s>-> aphraseboundary at the element start.<say-as interpret-as=...>...</say-as>-> the enclosed text is routed through :func:openfacefx.qa.normalize_transcript(the #23 ASCII fold).
Deferred / out of scope: <phoneme alphabet="ipa" ph="..."> pronunciation
override belongs to the multi-language framework (#8) + ipa.py -- its text is
passed through here and the ph ignored. Any unknown element degrades to its
text content and never crashes; malformed XML raises a clear ValueError at
the boundary. Deterministic and stdlib-only (xml.etree / re); numpy is
never imported, so <speak>hello world</speak> with no constructs yields
("hello world", []) -- byte-identical to the plain naive path.
EMPHASIS_STRENGTH: Dict[str, str] = {'strong': '1.0', 'moderate': '0.5', 'reduced': '0.0', 'none': '0.0'}
module-attribute
¶
BREAK_STRENGTH: Dict[str, float] = {'none': 0.0, 'x-weak': 0.1, 'weak': 0.25, 'medium': 0.5, 'strong': 0.75, 'x-strong': 1.0}
module-attribute
¶
looks_like_ssml(text: str) -> bool
¶
True if text opens with a <speak> root -- the auto-detect the naive
command uses to enable SSML parsing without an explicit --ssml flag.
parse_ssml(text: str) -> Tuple[str, List[Tag]]
¶
Parse an SSML document into (clean_text, tags).
Returns the same pair :func:openfacefx.texttags.parse_tagged_transcript
yields for the equivalent bracket transcript, ready for the unchanged naive
pipeline: clean_text is the spoken words (aliases substituted, say-as
normalized, tags removed) and tags the deterministic :class:Tag list
anchored to word indices in clean_text. Raises ValueError on malformed
XML (or a DOCTYPE); unknown elements pass through as their text content.