Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ssml Laravel Package

techworker/ssml

techworker/ssml is a PHP library for building SSML markup to control text-to-speech output. Use its fluent API to add breaks, emphasis, prosody, and phonemes for Amazon Polly, Google Cloud, and Azure TTS.

Deep Wiki
Context7

Getting Started

Install it like any small utility library:

composer require techworker/ssml

There is no service provider, config file, or facade to publish — this is a plain value-object library that builds a string. That means "onboarding" is really just reading src/ for 10 minutes. Start at the top-level builder/Speak class and the abstract element base class, then skim the concrete element classes (Break, Prosody, Emphasis, SayAs, Voice, P, S, Sub, Phoneme, etc.). Those constitute the entire public surface.

The first thing to build is the smallest possible document you'd actually send to a TTS engine:

use Techworker\Ssml\Ssml;
use Techworker\Ssml\Element\Speak;
use Techworker\Ssml\Element\Break;
use Techworker\Ssml\Element\Text;

$ssml = (new Ssml())->add(
    new Speak([
        new Text('Welcome back.'),
        new Break('600ms'),
        new Text('Let’s get started.'),
    ])
);

echo $ssml->xml(); // or (string) $ssml, depending on the version

Validate that output against a real engine immediately — drop it into an Amazon Polly SynthesizeSpeech call, or the Google/Azure equivalent — before writing any application code around it. The library will happily emit markup that is structurally valid XML but rejected or partially ignored by a specific engine.

Your first real use case is almost always the same: take user-facing content (email copy, notification text, IVR script), and wrap it with Prosody for pacing and Break for pauses so it doesn't sound like a robot reading a CSV.

Implementation Patterns

Compose small element trees, not giant builders. The library is designed around nesting element objects. Keep the composition close to the data:

private function greeting(string $name): ElementInterface
{
    return new Speak([
        new Prosody(['rate' => 'slow'], [
            new Text("Hello, {$name}."),
        ]),
    ]);
}

Wrap it in a domain-level SpeechScript service. Don't let controllers or jobs touch the SSML namespace directly. Build a thin service that takes your application's own DTOs (Alert, InvoiceReminder, etc.) and returns a rendered SSML string. That gives you one place to change when you swap engines or when someone asks "can we slow down the numbers?"

Escape text at the boundary. Element constructors generally accept raw strings and serialize them with htmlspecialchars-style escaping. Confirm this in your version — if it doesn't escape, you must wrap every dynamic value yourself. A single & or < in user content will corrupt the document and the engine error will be unhelpful. A defensive pattern is:

new Text($userContent) // preferred, IF the library escapes

and if it doesn't, new Text(htmlspecialchars($userContent, ENT_XML1)).

Integration tip: keep the SSML build separate from the TTS call. Render to a string, log it at debug level, then pass it to your provider SDK. This gives you a copy-paste-able artifact when a voice sounds wrong, and it lets you unit-test the markup without mocking the provider.

$markup = $script->render();
Log::debug('ssml', ['markup' => $markup]);
$audio = $polly->synthesizeSpeech([
    'Text' => $markup,
    'TextType' => 'ssml',
    'VoiceId' => $voiceId,
    'OutputFormat' => 'mp3',
]);

Template it for multi-voice dialogue. If you're producing podcast-style or IVR content, build one Speak per speaker and concatenate audio, or nest multiple Voice elements inside a single Speak when your engine supports it (Polly and Azure do; Google is stricter). Encapsulate the "which engine are we on" decision in the service, because the same logical script often needs slightly different markup per provider.

Test the output as XML, not as strings. Assert structure with assertXmlStringEqualsXmlString or by loading into DOMDocument and using XPath. String equality tests break on every whitespace or attribute-ordering change the library makes between patch versions.

$doc = new DOMDocument();
$doc->loadXML($script->render());
$xpath = new DOMXPath($doc);
$this->assertSame('600ms', $xpath->evaluate('string(//break/@time)'));

Cache rendered SSML, not audio. The markup depends on text, voice, and rate. If a reminder template is reused, rendering cost is trivial, but caching the string in Redis means downstream provider calls are easier to dedupe and you get a stable artifact to compare against.

Gotchas and Tips

Engine support is not tag support. The library models the full SSML 1.0 spec, but Polly ignores <emphasis> on most voices, Google rejects arbitrary <phoneme> alphabets, and Azure has its own mstts: extensions the library won't produce. Always check your rendered markup against the target engine's reference before shipping a new element into production.

Character limits are per request and count the markup. Polly caps at 3,000 billed characters (6,000 for standard/neural with caveats), Google at 5,000 bytes. XML tags count toward the byte/char budget in some providers. Long text needs chunking at sentence boundaries — and you must chunk before SSML generation, not by slicing the rendered string.

Break timing units are inconsistent across engines. Prefer explicit units (500ms, 1s) over bare numbers, and don't assume strength="strong" means the same thing on Polly as it does on Azure.

Watch out for empty or whitespace-only text nodes. Several engines will error on an empty <speak> or a <p> containing only whitespace. Trim and skip empty segments in your service layer rather than relying on the library to do it.

Ordering is not arbitrary. <speak> children render in insertion order. If you're appending via a builder-style API, remember that every ->add() mutates and there's no "insert before" — restructure your code to build arrays top-down rather than incrementally if you need ordering flexibility.

No dependency injection or config means testing is trivial — lean into it. You can instantiate the whole tree in-process with no fixtures. But that also means there's no logging, no error types, and no validation: malformed input produces malformed XML silently. Add a try { $doc->loadXML(...) } guard around your service if input is user-supplied.

Extending is straightforward but unsupported. For an engine-specific tag the package doesn't ship, subclass the base element (or implement its interface) and produce a raw tag. That's the intended extension point, but there's no guarantee a future minor version won't rename the base class — pin the composer constraint and add a test that exercises your custom element after every update.

Library is small and low-activity. With ~10 stars and no visible release cadence, treat it as a stable primitive rather than something that tracks new SSML features. It's MIT and tiny; forking or vendoring is a legitimate option if you need a specific tag and don't want to wait. The codebase is simple enough that a developer can read it end-to-end and be comfortable owning it.

Stringify once, pass around as a string. Element objects are mutable in most builder-style small libraries; don't stash a partially-built Speak in a singleton and keep adding to it. Build, render, discard the object tree.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib