SSMD Syntax Reference
This page provides a complete reference for SSMD markup syntax.
YAML front matter
An SSMD document may begin with a YAML front matter block. The opening delimiter must be
exactly --- on the first line; the closing delimiter is exactly --- or .... Four
hyphens are ordinary content. Front matter is metadata, not spoken text, and the YAML
root must be a mapping.
---
ssmd_version: "0.9"
voice_bindings:
kokoro:
moderator: af_sarah
pause_defaults:
enabled: true
sentence: 250ms
paragraph: 700ms
---
The optional title front-matter key is portable document metadata. It must be a string
and is preserved by formatting and authoring commands, but it is never spoken or
included in plain-text or SSML speech output.
---
ssmd_version: "0.9"
title: Review podcast
---
Hello world.
voice values in body directives and annotations are stable references. A reference can
be logical and resolved through voice_bindings, or direct when it is a concrete
provider voice ID. Effective precedence is built-in defaults, user config, document
header, then explicit CLI/API overrides. The local config file is an authoring facility
and is not part of the portable document.
pause_defaults accepts non-negative ms or s durations. Explicit body break markers
take precedence; defaults do not insert visible pause markers into SSMD source. PyYAML
serialization is deterministic but does not preserve YAML comments.
Document versions and dialects
An optional ssmd_version front-matter field selects the document dialect. Unversioned
documents retain the legacy compatibility behavior; "0.8" identifies the legacy
dialect, and "0.9" selects the strict SSMD 0.9 grammar and canonical structure.
Version values are preserved by formatting and migration writes "0.9" only after
checking semantic equivalence.
---
ssmd_version: "0.9"
---
Hello [world]{lang="en"}.
The CLI accepts --dialect auto|0.8|0.9 on lint and SSMD-to-SSML conversion commands.
auto uses the declared version and preserves the unversioned compatibility default.
Rendering targets are selected separately: generic for portable SSML, ssml-1.1 for
standard SSML 1.1, or provider for capability-specific adaptation.
--loss-policy error|warn|drop makes losses explicit: reject them, report them, or
allow them with informational diagnostics.
Semantic language vs pronunciation language
Language annotations are semantic by default:
[Bonjour]{lang="fr"}
Use scope="pronunciation" when the language should affect only pronunciation/G2P:
[File]{lang="en" scope="pronunciation"}
[Manpower]{lang="en" scope="pronunciation"}diskussion
ge[cancel]{lang="en" scope="pronunciation"}t
[download]{lang="en" scope="pronunciation"}en
The canonical 0.9 spelling is lang; legacy language and voice-lang aliases are
compatibility-only and should be converted with ssmd migrate. Omitted scope means
semantic, and the valid scopes are semantic and pronunciation. SSMD validates
BCP-47 tags but does not infer a document language.
Portable language-detection hint
A document may carry a consumer-facing routing hint in front matter:
---
language_detection:
mode: auto
languages: [de, en]
---
mode is off or auto; auto requires at least two distinct language entries. SSMD
validates and exposes this metadata, excludes it from clean text, and does not run
detection itself. It is not a local authoring-config default.
Text and Emphasis
SSMD 0.9 supports moderate, strong, and reduced emphasis using canonical Markdown-like syntax.
Moderate Emphasis
Use single asterisks for moderate (default) emphasis:
ssmd.to_ssml("This is *important*")
# → <speak>This is <emphasis>important</emphasis></speak>
Strong Emphasis
Use double asterisks for strong emphasis:
ssmd.to_ssml("This is **very important**")
# → <speak>This is <emphasis level="strong">very important</emphasis></speak>
Reduced Emphasis
Use double tildes for reduced (subtle) emphasis:
ssmd.to_ssml("This is ~~less important~~")
# → <speak>This is <emphasis level="reduced">less important</emphasis></speak>
Plain, unannotated text is already spoken without added emphasis; no explicit
emphasis="none" annotation is needed in canonical 0.9.
Breaks and Pauses
Time-Based Breaks
Specify duration in milliseconds or seconds using ... followed by a time value:
ssmd.to_ssml("Wait ...500ms please")
# → <speak>Wait <break time="500ms"/> please</speak>
ssmd.to_ssml("Wait ...2s please")
# → <speak>Wait <break time="2s"/> please</speak>
Note
Bare ... (without a time or strength code) is NOT treated as a break. It
will be preserved as literal ellipsis in your text. :::
Strength-Based Breaks
Use strength codes for semantic pauses:
ssmd.to_ssml("Hello ...n world") # none
ssmd.to_ssml("Hello ...w world") # weak (x-weak)
ssmd.to_ssml("Hello ...c world") # comma (medium)
ssmd.to_ssml("Hello ...s world") # sentence (strong)
ssmd.to_ssml("Hello ...p world") # paragraph (x-strong)
Strength codes:
n- nonew- weak (x-weak)c- comma (medium)s- sentence (strong)p- paragraph (x-strong)
Standalone Scene Separators
Strict SSMD 0.9 accepts a whole body line of exactly three hyphens, optionally
surrounded by spaces or tabs, as a compatibility scene break. Front-matter delimiters
are unaffected, and inline dashes such as Before --- after. remain text. A separator
contributes no spoken text and produces one x-strong break; repeated separators remain
separate events, while leading and trailing events sit at the start and end of clean
text. Canonical formatting writes ...p instead of ---.
To speak a literal whole-line sequence of three hyphens, escape the first hyphen with a backslash:
\---
Paragraphs
Blank lines separate paragraphs:
text = """
This is the first paragraph.
Still in first paragraph.
This is the second paragraph.
"""
ssmd.to_ssml(text)
# → <speak>This is the first paragraph.
# Still in first paragraph.
# This is the second paragraph.</speak>
Headings
Use hash marks for headings (configurable):
from ssmd import Document
text = """
# Main Title
Content here.
## Subtitle
More content.
"""
doc = Document(text, config={
'heading_levels': {
1: [('emphasis', 'strong'), ('pause', '500ms')],
2: [('emphasis', 'moderate')]
}
})
ssml = doc.to_ssml()
Annotations
Annotations use the format [text]{key="value"} where annotations can be:
Language Codes
Specify language with ISO codes:
# Auto-complete to full locale
ssmd.to_ssml('[Bonjour]{lang="fr"}')
# → <speak><lang xml:lang="fr-FR">Bonjour</lang></speak>
# Explicit locale
ssmd.to_ssml('[Hello]{lang="en-GB"}')
# → <speak><lang xml:lang="en-GB">Hello</lang></speak>
Common language codes:
en→ en-USfr→ fr-FRde→ de-DEes→ es-ESit→ it-ITja→ ja-JPzh→ zh-CNru→ ru-RU
Voice Selection
Use inline annotations for short selections and fenced directives for sustained
dialogue. voice names a logical or concrete voice; feature selectors use voice-name,
voice-languages, gender, age, and variant.
[Hello]{voice="host"}
[Bonjour]{voice-languages="fr-FR" gender="female"}
[Hello]{voice-name="en-US-Wavenet-A" voice-languages="en-US"}
:::{voice="host"}
Welcome to Tech Talk. This entire block uses the host voice.
:::
:::{voice="guest"}
Thanks for having me.
:::
Fenced directives require at least three colons on both the opening and matching
closing fence. A two-colon opener such as ::{voice="host"} is invalid; use
:::{voice="host"}. Logical references may be resolved through the portable
voice_bindings front-matter key or local trusted configuration. Voice selectors are
independent of provider inventory data. Supported feature selectors are preserved when
rendering or reported as losses if the selected target cannot represent them.
Adjacent sibling fenced directives with no blank line between remain in the same semantic paragraph: their clean text receives ordinary inline separation and no paragraph event. A blank line between the directives creates a paragraph boundary. Canonical formatting preserves this difference; a voice change alone does not add a paragraph pause.
Raw <div> voice blocks and voice-lang are compatibility-only 0.8 syntax. New 0.9
documents use canonical ::: directives and voice-languages.
Phonetic Pronunciation
IPA (International Phonetic Alphabet)
ssmd.to_ssml('[tomato]{ph="təˈmeɪtoʊ"}')
# → <speak><phoneme alphabet="ipa" ph="təˈmeɪtoʊ">tomato</phoneme></speak>
ssmd.to_ssml('[hello]{ipa="həˈloʊ"}')
# → <speak><phoneme alphabet="ipa" ph="həˈloʊ">hello</phoneme></speak>
X-SAMPA (Extended Speech Assessment Methods Phonetic Alphabet)
ssmd.to_ssml('[dictionary]{sampa="dIkS@n@ri"}')
# → <speak><phoneme alphabet="x-sampa" ph="dIkS@n@ri">dictionary</phoneme></speak>
Substitution (Aliases)
Replace text with alternative pronunciation:
ssmd.to_ssml('[H2O]{sub="water"}')
# → <speak><sub alias="water">H2O</sub></speak>
ssmd.to_ssml('[AWS]{sub="Amazon Web Services"}')
# → <speak><sub alias="Amazon Web Services">AWS</sub></speak>
ssmd.to_ssml('[NATO]{sub="North Atlantic Treaty Organization"}')
Say-As Interpretations
Control how text is interpreted:
# Telephone number
ssmd.to_ssml('[+1-555-0123]{as="telephone"}')
# Date with format
ssmd.to_ssml('[31.12.2024]{as="date" format="dd.mm.yyyy"}')
# Say-as with detail attribute (verbosity control)
ssmd.to_ssml('[123]{as="cardinal" detail="2"}')
# → <speak><say-as interpret-as="cardinal" detail="2">123</say-as></speak>
ssmd.to_ssml('[12/31/2024]{as="date" format="mdy" detail="1"}')
# → <speak><say-as interpret-as="date" format="mdy" detail="1">12/31/2024</say-as></speak>
# Spell out characters
ssmd.to_ssml('[NASA]{as="character"}')
# Number types
ssmd.to_ssml('[123]{as="cardinal"}') # one hundred twenty-three
ssmd.to_ssml('[1st]{as="ordinal"}') # first
ssmd.to_ssml('[123]{as="digits"}') # one two three
ssmd.to_ssml('[3.14]{as="fraction"}') # three point one four
# Time
ssmd.to_ssml('[14:30]{as="time"}')
# Expletive (censored/beeped)
ssmd.to_ssml('[damn]{as="expletive"}')
Supported interpret-as values:
character- Spell outcardinal- Numberordinal- First, second, etc.digits- Individual digitsfraction- Decimal numbersunit- Measurementsdate- Datestime- Time valuestelephone- Phone numbersaddress- Street addressesexpletive- Censored words
The detail attribute (1-2) controls verbosity level and is platform-specific. Higher
values generally provide more detailed pronunciation.
Prosody (Voice Control)
Use prosody annotations with explicit key/value pairs:
ssmd.to_ssml('[loud]{volume="loud"}')
ssmd.to_ssml('[slow]{rate="slow"}')
ssmd.to_ssml('[high]{pitch="high"}')
ssmd.to_ssml('[loud and fast]{volume="loud" rate="fast"}')
Scale-Based Values (1-5)
ssmd.to_ssml('[extra loud]{volume="5"}')
ssmd.to_ssml('[extra fast]{rate="5"}')
ssmd.to_ssml('[extra high]{pitch="5"}')
Scale mapping:
Volume: 0=silent, 1=x-soft, 2=soft, 3=medium, 4=loud, 5=x-loud
Rate: 1=x-slow, 2=slow, 3=medium, 4=fast, 5=x-fast
Pitch: 1=x-low, 2=low, 3=medium, 4=high, 5=x-high
Compatibility-only prosody aliases
The following forms are accepted only in legacy/unversioned compatibility mode and are
not canonical SSMD 0.9 syntax: compact vrp, short v/r/p keys, punctuation
prosody, and symbolic delimiters such as ++text++. Strict 0.9 parsing diagnoses these
forms. Use explicit volume, rate, and pitch attributes in new documents. Run
ssmd migrate FILE --to 0.9 for a semantics-checked conversion of legacy input.
Relative Values
# Decibels for volume
ssmd.to_ssml('[louder]{volume="+6dB"}')
ssmd.to_ssml('[quieter]{volume="-3dB"}')
# Percentages for rate and pitch
ssmd.to_ssml('[faster]{rate="+20%"}')
ssmd.to_ssml('[slower]{rate="-10%"}')
ssmd.to_ssml('[higher]{pitch="+15%"}')
ssmd.to_ssml('[lower]{pitch="-5%"}')
Audio Sources
Basic Audio
# With description
ssmd.to_ssml('[doorbell]{src="https://example.com/sounds/bell.mp3"}')
# → <audio src="https://example.com/sounds/bell.mp3"><desc>doorbell</desc></audio>
# No description
ssmd.to_ssml('[]{src="beep.mp3"}')
# → <audio src="beep.mp3"></audio>
Application-resolved audio sources
The src attribute is a non-empty source string. It may use a renderer- or
application-specific URI scheme; SSMD preserves the value without resolving it or
checking whether a renderer supports it. sfx: is one application-defined scheme, not a
scheme interpreted by SSMD. Rendering support and URI resolution belong to the consuming
application.
[three knocks]{src="sfx:impact.knock?material=oak&count=3&force=0.7&seed=42" desc="Three knocks"}
[spoken fallback]{src="sfx:impact.knock?seed=42" desc="Knocking"}
For audio annotations, the annotation content is spoken fallback text and desc is
descriptive metadata; desc does not replace the fallback. Generic audio controls such
as clip, speed, repeat, repeatdur, and level remain SSMD attributes.
Effect-specific parameters belong inside the application-defined URI. SSMD does not
generate sounds, validate effect catalogs or parameters, fetch sources, or cache
rendered media.
Audio with Fallback
ssmd.to_ssml('[cat purring]{src="cat.ogg" alt="Sound file not loaded"}')
# → <audio src="cat.ogg"><desc>cat purring</desc>Sound file not loaded</audio>
If an audio source cannot be played, the annotation content provides its spoken fallback.
Advanced Audio Attributes
SSMD supports advanced audio control through SSML attributes:
Audio Clipping
Play a portion of an audio file by specifying start and end times:
ssmd.to_ssml('[music]{src="song.mp3" clip="5s-30s"}')
# → <audio src="song.mp3" clipBegin="5s" clipEnd="30s"><desc>music</desc></audio>
ssmd.to_ssml('[intro]{src="podcast.mp3" clip="0s-10s"}')
# → <audio src="podcast.mp3" clipBegin="0s" clipEnd="10s"><desc>intro</desc></audio>
Speed Control
Adjust playback speed using percentages:
ssmd.to_ssml('[announcement]{src="speech.mp3" speed="150%"}')
# → <audio src="speech.mp3" speed="150%"><desc>announcement</desc></audio>
ssmd.to_ssml('[slow]{src="message.mp3" speed="80%"}')
# → <audio src="message.mp3" speed="80%"><desc>slow</desc></audio>
Repeat Audio
Repeat audio playback a specific number of times:
ssmd.to_ssml('[jingle]{src="ad.mp3" repeat="3"}')
# → <audio src="ad.mp3" repeatCount="3"><desc>jingle</desc></audio>
ssmd.to_ssml('[beep]{src="alert.mp3" repeat="5"}')
# → <audio src="alert.mp3" repeatCount="5"><desc>beep</desc></audio>
Volume Adjustment
Control audio volume using decibel adjustment:
ssmd.to_ssml('[alarm]{src="alert.mp3" level="+6dB"}')
# → <audio src="alert.mp3" soundLevel="+6dB"><desc>alarm</desc></audio>
ssmd.to_ssml('[background]{src="music.mp3" level="-3dB"}')
# → <audio src="music.mp3" soundLevel="-3dB"><desc>background</desc></audio>
Combining Attributes
Multiple audio attributes can be combined with fallback text:
ssmd.to_ssml('[bg music]{src="music.mp3" clip="0s-10s" speed="120%" level="-3dB" alt="Fallback text"}')
# → <audio src="music.mp3" clipBegin="0s" clipEnd="10s" speed="120%" soundLevel="-3dB">
# <desc>bg music</desc>Fallback text</audio>
ssmd.to_ssml('[effect]{src="sound.mp3" clip="2s-5s" repeat="2" alt="Sound unavailable"}')
# → <audio src="sound.mp3" clipBegin="2s" clipEnd="5s" repeatCount="2">
# <desc>effect</desc>Sound unavailable</audio>
Note
Audio attribute support varies by TTS platform. Amazon Polly and Google Cloud TTS support most of these features. Always test with your specific TTS engine. :::
Markers
Markers create synchronization points for events:
ssmd.to_ssml('I always wanted a @animal cat as a pet.')
# → <speak>I always wanted a <mark name="animal"/> cat as a pet.</speak>
ssmd.to_ssml('Click @here to continue.')
# → <speak>Click <mark name="here"/> to continue.</speak>
Markers are removed when stripping to plain text:
ssmd.to_text('Click @here now')
# → Click now
Extensions
Platform-specific extensions allow you to use TTS features beyond standard SSML.
Amazon Polly Extensions
Amazon Polly provides effects like whispering and dynamic range compression:
# Whisper effect
ssmd.to_ssml('[secret message]{ext="whisper"}')
# → <amazon:effect name="whispered">secret message</amazon:effect>
# Dynamic range compression (for voice over music)
ssmd.to_ssml('[announcement]{ext="drc"}')
# → <amazon:effect name="drc">announcement</amazon:effect>
Google Cloud TTS Speaking Styles
Google Cloud TTS supports speaking styles for Neural2 and Studio voices. You can configure these using SSMD’s extension system:
from ssmd import Document
# Configure Google TTS styles as extensions
doc = Document(config={
'extensions': {
'cheerful': lambda text: f'<google:style name="cheerful">{text}</google:style>',
'calm': lambda text: f'<google:style name="calm">{text}</google:style>',
'empathetic': lambda text: f'<google:style name="empathetic">{text}</google:style>',
'apologetic': lambda text: f'<google:style name="apologetic">{text}</google:style>',
'firm': lambda text: f'<google:style name="firm">{text}</google:style>',
}
})
# Use styles in your content
doc.add_sentence("[Welcome to our service!]{ext=\"cheerful\"}")
doc.add_sentence("[We apologize for the inconvenience.]{ext=\"apologetic\"}")
doc.add_sentence("[Please remain calm.]{ext=\"calm\"}")
ssml = doc.to_ssml()
# → <speak>
# <google:style name="cheerful">Welcome to our service!</google:style>
# <google:style name="apologetic">We apologize for the inconvenience.</google:style>
# <google:style name="calm">Please remain calm.</google:style>
# </speak>
Available Google TTS speaking styles:
cheerful- Upbeat and positive tonecalm- Relaxed and soothing toneempathetic- Understanding and compassionate toneapologetic- Sorry and regretful tonefirm- Confident and authoritative tonenews- Professional news anchor tone (some voices)conversational- Natural conversation tone (some voices)
Note
Google TTS speaking styles are only supported by specific Neural2 and Studio voices. See the Google Cloud TTS documentation for voice compatibility. :::
Custom Extensions
You can define your own extensions for any custom SSML tags your TTS platform supports:
from ssmd import Document
doc = Document(config={
'extensions': {
'robotic': lambda text: f'<voice-transformation type="robot">{text}</voice-transformation>',
'echo': lambda text: f'<audio-effect type="echo">{text}</audio-effect>',
}
})
doc.add_sentence("[Hello]{ext=\"robotic\"}")
doc.add_sentence("[world]{ext=\"echo\"}")
For a complete Google TTS styles example, see examples/google_tts_styles.py.
Combining Multiple Annotations
Multiple annotations can be space-separated inside the braces:
ssmd.to_ssml('[Bonjour]{lang="fr" volume="5" rate="2"}')
# → <lang xml:lang="fr-FR"><prosody volume="x-loud" rate="slow">Bonjour</prosody></lang>
ssmd.to_ssml('[important]{volume="5" as="character"}')
# → <prosody volume="x-loud"><say-as interpret-as="character">important</say-as></prosody>
Escaping
XML Special Characters
XML special characters are automatically escaped:
ssmd.to_ssml('5 < 10 & 10 > 5')
# → <speak>5 < 10 & 10 > 5</speak>
Security
All user input is automatically sanitized to prevent XML injection attacks. Special characters in both text content and annotation parameters are properly escaped:
# Malicious input is safely escaped
ssmd.to_ssml('[text]{sub="value<script>alert(1)</script>"}')
# → <speak><sub alias="value<script>alert(1)</script>">text</sub></speak>
The library ensures:
XML validity: Output is always valid, well-formed XML
Injection prevention: User input cannot break out of attribute values or inject tags
Automatic escaping: All special characters (
<,>,&,",') are escaped
You can safely use SSMD with untrusted user input in TTS applications.
Literal Asterisks
To include literal asterisks without emphasis, escape them or use different patterns:
# These won't be treated as emphasis
ssmd.to_ssml('2 * 3 = 6')
# → <speak>2 * 3 = 6</speak>
ssmd.to_ssml('* list item')
# → <speak>* list item</speak>
Voice Defaults and Natural Prosody
Use front matter to keep identity-defining prosody with a logical voice:
---
ssmd_version: "0.9"
voice_defaults:
guest:
pitch: high
rate: normal
prosody_transitions:
enabled: true
same_voice_only: true
rate: 450ms
pitch: 300ms
---
A block or inline voice reference inherits these values without changing the formatted source:
:::{voice="guest"}
Hello.
A local attribute overrides only that field. For example, `rate="slow"` keeps the guest
pitch default. Inline annotations and voice blocks use the same logical voice lookup.
Provider bindings do not change the key used by `voice_defaults`.
The natural rate values are `very-slow`, `slow`, `moderate`, `normal`, `brisk`, `fast`,
and `very-fast`, mapped respectively to `65%`, `80%`, `90%`, `100%`, `110%`, `125%`, and
`150%`. Natural pitch values are `very-low`, `low`, `moderate-low`, `normal`,
`moderate-high`, `high`, and `very-high`, mapped to `-20%`, `-12%`, `-6%`, `+0%`, `+6%`,
`+12%`, and `+20%`. Explicit percentages are supported; compact `vrp` is
compatibility-only.
Formatting preserves declared attributes and does not materialize inherited defaults.
Use inspection to view both forms:
```bash
ssmd --json inspect episode.ssmd.md --sentences
The JSON sentence view includes declared_prosody, effective_prosody, and per-field
sources.