Skip to documentation
Browse documentation

Text to speech · alignment

Speech and timestamps, together.

Add return_timestamps: true to a buffered speech request or queued job. Download the audio as usual, then read its measured English word or word-group intervals. This option uses the v3 alignment pipeline.

1. Generate audio with alignment enabled

The default is false. MP3 and WAV are supported; MP3 supports speed 0.5–2 and WAV uses speed 1. Alignment uses the same prepared audio clock as the returned file, including MP3 speed changes.

cURL · opt in
curl https://voice.castreader.com/v1/audio/speech \
  -H "Authorization: Bearer $CASTREADER_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: speech-with-timestamps-0001' \
  --data '{"model":"clone-v1","voice_id":"voice_YOUR_ID",
    "text":"Hello from CastReader.","language":"en",
    "output_format":"mp3","return_timestamps":true}' \
  --fail-with-body --dump-header speech.headers --output speech.mp3

A successful response is still binary audio, with X-Request-ID and X-Timestamps-URL headers. Check HTTP 200 before playing the downloaded file; error responses contain JSON. Alignment adds processing time. Use queued speech when you want the service to wait for capacity.

2. Read the time intervals

Call GET /v1/requests/{request_id}/timestamps with the same project’s API key and speech:generate permission. The request receipt also includes timestamps_url. Retrieving metadata does not generate audio or deduct credits.

cURL · read metadata
# Copy X-Request-ID from speech.headers after a successful HTTP 200.
export REQUEST_ID="req_YOUR_REQUEST_ID"
curl "https://voice.castreader.com/v1/requests/$REQUEST_ID/timestamps" \
  -H "Authorization: Bearer $CASTREADER_API_KEY" \
  --fail-with-body --output speech.timestamps.json
JSON · example; timings vary by generation
{
  "object": "speech.timestamps",
  "request_id": "req_YOUR_REQUEST_ID",
  "status": "aligned",
  "alignment_revision": "qwen-pcm-measured-groups-v3",
  "unit": "seconds",
  "granularity": "word",
  "duration_seconds": 1.296,
  "words": [
    {
      "word": "Hello",
      "start_time": 0,
      "end_time": 0.32
    },
    {
      "word": "from",
      "start_time": 0.32,
      "end_time": 0.56
    },
    {
      "word": "CastReader",
      "start_time": 0.56,
      "end_time": 1.2
    }
  ],
  "reason": null
}
  • start_time and end_time are seconds from the start of this audio file. They are not task creation dates, character offsets or milliseconds.
  • granularity: word means measured word intervals. word_group means v3 kept some adjacent words together because separate boundaries could not be resolved. Render the whole group; do not divide its time evenly into fabricated word timestamps.
  • words[].word preserves source text within the measured span. Leading, trailing and between-word punctuation may be omitted. Keep your original text for display.
  • Intervals are ordered, non-overlapping and within the audio duration. Silence can create gaps. Sentence-level alignment, SRT and WebVTT export are not separate API outputs.

Queued jobs and source clients

Use the same option in POST /v1/jobs. Once a chunk succeeds, its requestId identifies both its audio and timestamps. Times start at zero for each chunk; sourceStart and sourceEnd are character offsets and cannot be used as audio times.

Node.js source client · queued job
const job = await client.createJob({
  model: 'clone-v1', voice_id: process.env.VOICE_ID,
  text: 'Hello from CastReader.', language: 'en',
  output_format: 'mp3', return_timestamps: true,
}, { idempotencyKey: 'caption-job-0001' });
// Poll getJob(job.id), respecting Retry-After and the task deadline.
const status = await client.getJob(job.id);
if (status.status === 'succeeded') {
  for (const chunk of status.chunks) {
    const captions = await client.getTimestamps(chunk.requestId);
    // Each chunk has its own audio clock. Check captions.status first.
    console.log(captions);
  }
}

The Node.js source client provides getTimestamps(requestId). Python provides get_timestamps(request_id); pass return_timestamps=True to speech or create_job. These are repository source clients, not published package names.

Failure, retries and billing

Check status before using timestamps. aligned contains measured intervals. unavailable has an empty words array, null granularity and a reason of alignment_timeout or alignment_unavailable. Duration may be null when the alignment step could not run. There is no guessed or evenly distributed fallback.

Alignment is an optional part of audio generation with no extra fee. Successfully delivered audio is billed once at the normal character price even if optional alignment is unavailable. Failed audio generation does not settle a generation charge. A metadata download is free; generating a new audio result is a new billable request.

Reuse the same idempotency key and unchanged body for retries. Turning timestamps on changes the input: use a new key for a new generation. Replays return the original audio and alignment result, including an unavailable result; they do not rerun the aligner.

Audio and timestamp metadata have the same retention window, currently 24 hours after successful generation. Both require current voice authorization. Metadata returns 404 if timestamps were not requested, 409 before completion and 410 after expiry. Revoking access or deleting the voice blocks access immediately.

Current support is English buffered MP3/WAV and queued jobs. Chinese, Japanese and other languages generate audio without alignment. Omit return_timestamps for non-English input; setting it to true returns 422 unsupported_timestamps before generation and billing. Streaming timestamps, transcription of arbitrary uploaded audio, and exact sentence-level timestamps are not offered by this endpoint.

OpenAPI specification · Capabilities and limits · Metering and billing