Speech marks
Overview
Speech marks provide a mapping between time and text. They inform the client when each word is spoken in the audio, enabling features like:
- Text highlighting during playback
- Precise audio seeking by text position
- Usage tracking and analytics
- Synchronization between text and audio
Two endpoints return them:
Use the streaming endpoint when you want highlighting to start before the
synthesis has finished. Use the batch endpoint when you want a single response,
or when you need a legacy model. POST /v1/audio/stream returns raw audio only
and is unchanged.
Data structure
Speech marks use the following TypeScript interfaces:
Important considerations
-
SSML escaping: Values are returned based on the SSML, so any escaping of
&,<and>will be present in thevalue,startandendfields. Consider using the string tracker library to assist with mapping. -
Index gaps: The
startandendvalues of each word may have gaps. When looking for a word at a specific index, check forstartbeing>= yourIndexrather than checking if the index is within bothstartandendbounds. -
Timing gaps: Similarly,
start_timeandend_timeof each word may have gaps. Follow the same approach as with index gaps. -
Initial silence: The
start_timeof the first word is not necessarily0like theSpeechMarks. There can be silence at the beginning of the sentence that leads to the word starting partway through. -
Trailing silence: The
end_timeof the last word does not necessarily correspond with the end of theSpeechMarks. There can be silence at the end that will make theSpeechMarkslonger.
Example output
For the input "Hello, welcome to Speechify", the response includes:
Note how start_time of the first word (125ms) doesn’t match the SpeechMarks start (0ms) - there’s initial silence before speech begins.
Streaming speech marks
POST /v1/audio/stream/with-timestamps takes the same request body as
POST /v1/audio/stream and returns a Server-Sent Events stream instead of raw
audio bytes.
Each event is named. speech.chunk carries a Base64-encoded run of audio, the
speech marks that became final with it, or both:
The marks use the same fields as the batch endpoint, so one parser handles both.
Consuming the stream
- Concatenate the audio. Base64-decode each
audiofield and append it. The result is byte-for-byte whatPOST /v1/audio/streamwould have returned. - Apply marks against that one timeline.
start_timeandend_timeare absolute milliseconds from the start of the synthesis. Which event a mark arrives on is a delivery detail and carries no meaning, so do not try to align a mark to the audio chunk it arrived with. - A chunk may carry only one of the two. Marks lag their audio slightly, and the last chunk of a stream is often marks-only.
- Ignore unknown event types. New event types may be added; a client that skips names it does not recognize keeps working.
- There is no
[DONE]sentinel.speech.doneis the terminal event.
Errors
Before the stream starts, failures are ordinary HTTP responses with the standard
error envelope. A request for a model that cannot produce speech marks fails here
with 400 speech_marks_unsupported - use simba-3.0 or simba-3.2, or call
POST /v1/audio/speech, which returns speech marks for every model.
Once the stream has started the status code is already committed, so a later failure arrives as a terminal event carrying the same envelope:
Audio format
output_format and the Accept header select the codec exactly as they do on
POST /v1/audio/stream. Because the response’s own Content-Type is
text/event-stream, the media type of the audio inside the events is echoed on
the Speechify-Audio-Content-Type response header.
Changing the codec or sample rate does not change the duration, so mark times
stay correct for every output_format.