Command Line Interface

SSMD ships a command-line tool for creating, validating, converting, and formatting SSMD files. The CLI uses Typer/Click for command registration and provides a root-level --json option for machine-readable output with stable success/error envelopes.

After installing SSMD, the ssmd command is available:

pip install ssmd
ssmd --version

You can also run it as python -m ssmd.

Exit codes

Exit code

Meaning

0

Success. No lint errors (warnings allowed unless --fail-on-warn).

1

Lint found one or more errors, or --fail-on-warn found warnings.

2

CLI usage error, unreadable input, invalid output path, or invalid profile/preset.

3

Fatal conversion or parse error.

Machine-readable output

Use --json at the root level to get JSON output:

ssmd --json lint story.ssmd.md
ssmd --json profiles
ssmd --json inspect story.ssmd.md --spans
ssmd --json voices list

The JSON output uses a stable envelope format:

{
  "schema": "ssmd.cli.v1",
  "ok": true,
  "command": "lint",
  "result_type": "lint_report",
  "result": { ... }
}

The schema value is ssmd.cli.v1 for both success and error envelopes. Consumers should check the schema before interpreting the command-specific payload. Error envelope:

{
  "schema": "ssmd.cli.v1",
  "ok": false,
  "command": "convert",
  "error": {
    "code": "USAGE_ERROR",
    "message": "...",
    "exit_code": 2
  }
}

For lint and format-check reports, result.passed or result.clean indicates whether the document passed validation. The outer ok indicates whether the CLI operation itself succeeded.

Agents must check both the process exit code and the command-specific result state:

Command

Required success state

create

exit 0, ok == true, result.created == true, output exists

lint / check

exit 0, ok == true, result.passed == true

fmt --check

exit 0, ok == true, result.clean == true

conversion with output

exit 0, ok == true, expected output exists or is reported written

config validate

exit 0, ok == true, command-specific valid state

ok == true only means that a domain result was returned. A warning-blocked create --fail-on-warn can return ok == true, result.created == false, and exit 1; it must not be treated as a successful shipment.

config and voices

The local authoring configuration is resolved in this order: root --config PATH, SSMD_CONFIG, then Click’s platform application directory. On Linux the default is ~/.config/ssmd/config.yaml. Read-only commands do not create the file.

ssmd --json config path
ssmd config init
ssmd --json config show --effective
ssmd --json config validate
ssmd --json voices list --provider kokoro
ssmd voices bind kokoro moderator af_sarah
ssmd --json voices resolve moderator --provider kokoro

voices list is deterministic and excludes disabled entries unless --include-disabled is supplied. Inventory entries are local authoring data and are not copied into portable document headers.

lint / check

Validate SSMD syntax and profile compatibility:

ssmd lint story.ssmd.md
ssmd check story.ssmd.md          # alias for lint

Options:

--profile NAME

: Lint profile to use (default ssmd-core). Available profiles: run ssmd profiles.

--dialect {auto,0.8,0.9}

: Select syntax dialect. auto uses ssmd_version when present and retains the legacy behavior for unversioned documents; explicit 0.9 enables strict 0.9 parsing.

--loss-policy {error,warn,drop}

: Choose how unsupported rendering semantics are handled during compatibility checks. error fails, warn reports a warning, and drop allows the conversion with an informational diagnostic. --capabilities PRESET

: Validate conversion against a TTS capability preset.

--format {text,json}

: Output format (default text). JSON emits a machine-readable report.

--fail-on-warn

: Exit 1 when warnings are found (useful in CI).

--quiet

: Suppress the path: ok line on success.

--no-xml-check

: Skip checking generated SSML for XML well-formedness.

--roundtrip

: Additionally compare canonical SSMD semantics across SSMD→SSML→SSMD. Equivalent block-level and inline voice representations are treated as the same semantics.

The comparison tolerates formatting-only whitespace changes but preserves spoken text, voice boundaries, supported annotations, marks, breaks, paragraph structure, and front-matter meaning. Reconstructed nested markup is emitted in a safe block form rather than becoming literal prose.

--parse-yaml-header / --no-yaml-header

: Front matter is parsed by default. Use --no-yaml-header for literal leading --- content; --parse-yaml-header remains a compatibility spelling.

--voice-provider PROVIDER

: Resolve voice references against one provider.

--no-config

: Perform portable structural lint without requiring local inventory entries.

Text output uses clean chars offsets (the clean-text coordinate system), not source line/column positions:

story.ssmd: error: clean chars 0-7: Tag 'inline' is not supported by profile 'ssmd-core'.
story.ssmd: warn: say-as 'currency' not supported, dropping

Parser diagnostics with a remediation hint print a following <path>: hint: ... line. Lint JSON issue objects include a hint field only when a hint is available.

create

Create a formatted and validated SSMD file with an atomic write:

ssmd create draft.ssmd.md -o episode.ssmd.md
cat draft.ssmd.md | ssmd create - -o episode.ssmd.md
ssmd create draft.ssmd.md -o episode.ssmd.md --fail-on-warn
ssmd create draft.ssmd.md -o episode.ssmd.md --force

create performs source formatting, syntax/profile validation, SSMD→SSML conversion, XML well-formedness validation, and a semantic SSMD→SSML→SSMD round-trip check before writing the output. If validation fails, the output file is not created or replaced. In JSON mode, successful creation requires result.created == true, nonzero result.bytes_written, and the requested output path to exist.

Options:

--profile NAME

: Lint profile to enforce (default ssmd-core).

--capabilities PRESET

: Validate against a target TTS capability preset.

--fail-on-warn

: Refuse to write when warnings are present.

--parse-yaml-header

: Compatibility spelling; YAML front matter is parsed by default.

--config PATH

: Select the local authoring configuration. SSMD_CONFIG is used when this option is absent, followed by Click’s platform application directory (~/.config/ssmd/config.yaml on Linux).

--voice-provider PROVIDER

: Select the active provider for voice binding materialization.

Create discovers canonical fenced directives and materializes required voice_bindings as defaults. Empty mappings and missing nested provider or role entries may be filled, while explicit document bindings remain authoritative. Raw <div> syntax is compatibility-only.

--bind REFERENCE=VOICE_ID

: Add a repeatable explicit binding override for the selected provider.

--materialize-config/--no-materialize-config

: Enable or disable create-time config-derived header fields (enabled by default).

--materialize-voice-bindings/--no-materialize-voice-bindings and --materialize-pause-defaults/--no-materialize-pause-defaults

: Override individual materialization categories.

--no-format

: Preserve source bytes instead of normalizing line endings.

--no-roundtrip

: Skip the semantic round-trip check.

--force

: Replace an existing output file. Replacement is atomic and preserves existing permissions.

convert

Convert between SSMD, SSML, and plain text:

ssmd convert story.ssmd.md --to ssml
ssmd convert story.ssml --from ssml --to ssmd -o story.ssmd.md
ssmd convert story.ssmd.md --to text
ssmd convert story.ssmd.md --to ssml -o story.ssml

Use .ssmd.md and .ssmd as unambiguous SSMD filename hints. SSML inputs with .ssml or .xml are inferred as SSML. A plain .md file is inferred as SSMD only when its front matter contains ssmd_version; otherwise specify --from explicitly. Explicit --from always takes precedence.

cat story.ssmd.md | ssmd convert - --from ssmd --to ssml

For SSMD-to-SSML conversions, --target {generic,ssml-1.1,provider} selects the serialization target. generic produces portable SSML, ssml-1.1 enforces SSML 1.1 requirements, and provider applies the selected capability profile.

--language and --fallback-language override the root language used for SSML output. The ssml-1.1 target requires a root language and a <speak> document wrapper; the CLI rejects --target ssml-1.1 combined with --no-speak-tag.

--loss-policy {error,warn,drop} controls unsupported semantics: error rejects lossy output, warn returns warnings, and drop permits losses with informational diagnostics. The optional --dialect {auto,0.8,0.9} selects the input syntax version; auto honors the document version and preserves the unversioned compatibility default.

For SSML-to-SSMD conversion, from-ssml and convert --from ssml reject unrepresentable semantics by default. warn and drop opt into reported losses. Results are complete, versioned SSMD 0.9 documents by default; use --fragment only when a body fragment is needed.

to-ssml / from-ssml / text

Convenience aliases for common conversions:

ssmd to-ssml story.ssmd.md -o story.ssml
ssmd from-ssml story.ssml -o story.ssmd.md
ssmd text story.ssmd.md

to-ssml accepts the same SSMD-to-SSML options as convert, including --target, --loss-policy, --language, --fallback-language, and --no-speak-tag. The strict SSML 1.1 target cannot be combined with --no-speak-tag.

Sentence detection options on convert and to-ssml are:

  • --sentence-spacy-model TEXT for an exact package.

  • --sentence-model-size sm|md|lg|trf for an exact tier.

  • --sentence-use-spacy or --no-sentence-use-spacy to force the backend.

When neither model nor size is set, SSMD uses phrasplit’s highest installed compatible model for the document language. JSON conversion results and inspect --header include the selected model diagnostics when sentence detection runs.

text --capabilities PRESET applies strict capability filtering before plain-text rendering. For example, unsupported substitutions remain as their source text.

fmt

fmt applies canonical formatting to explicitly versioned SSMD 0.9. Unversioned and 0.8 documents retain their dialect and receive source-preserving line-ending normalization only; fmt never migrates input. Use ssmd migrate for an explicit semantic-equivalence-checked upgrade.

ssmd fmt story.ssmd.md            # formatted output to stdout
ssmd fmt story.ssmd.md -w         # write normalized result in place, atomically
ssmd fmt story.ssmd.md --check    # exit 1 if formatting would change
ssmd fmt a.ssmd.md b.ssmd.md -w      # format multiple files

Without -w or --check, formatted SSMD is written to stdout. Multiple files require either -w or --check; those two modes are mutually exclusive. Stdin cannot be combined with -w, and - may appear only once.

migrate

Migrate a legacy document to canonical SSMD 0.9 only when semantic equivalence is verified:

ssmd migrate legacy.ssmd --to 0.9                 # canonical content to stdout
ssmd migrate legacy.ssmd --to 0.9 -o story-09.ssmd.md
ssmd migrate legacy.ssmd --to 0.9 --write        # replace in place atomically
ssmd --json migrate legacy.ssmd --to 0.9           # inspect migration result

Migration never modifies the source unless --write or an output path is explicit. Existing output files are not replaced without --overwrite. Documents that require a semantic decision are left unchanged, with diagnostics and manual actions returned instead of an unsafe rewrite.

Migration preserves clean text, effective annotation coverage, structural events, and portable header data. Block-aligned legacy <div> scopes are emitted as fenced directives. When fencing would introduce paragraph boundaries absent from the legacy structure, migration can use paragraph-local inline annotations with separators outside the annotations. Invalid source syntax is reported as a source error. An invalid generated 0.9 candidate is a migration error, not a request to edit syntax that was absent from the source.

profiles

List available lint profiles and capability presets:

ssmd profiles
ssmd profiles --json

inspect (JSON)

Inspect parsed structure (useful for debugging and TTS integrations):

ssmd inspect story.ssmd.md --spans
ssmd inspect story.ssmd.md --sentences
ssmd inspect story.ssmd.md --paragraphs

Output is always JSON.

inspect --sentences reports both author declarations and resolved rendering values for voice prosody. Each sentence includes declared_prosody, effective_prosody, and sources; source values identify inline, directive, inherited directive, or voice_default provenance. inspect --header exposes the parsed voice_defaults and prosody_transitions metadata.

voice_defaults is keyed by logical SSMD voice names and does not contain provider bindings. prosody_transitions is preserved as a renderer hint. It does not add a non-standard transition element to generated SSML.

version

Print the installed SSMD version:

ssmd version
ssmd --version