---
title: Word timestamps
description: LMNT's models return word timestamps, enabling you to sync subtitles, lip movement, other modalities, and more with your generated speech.
---

If you're producing video content, you often want to show subtitles.

Use LMNT to get exact word timing with precisely the words being spoken,
instead of relying on external subtitle providers that try to guess and may
confuse similar sounding words.

## Getting timestamps with the Speech API

<CodeGroup>
```python Python
import asyncio
import base64

from lmnt import AsyncLmnt

async def main():
  client = AsyncLmnt()
  response = await client.speech.generate_detailed(
    text=(
      "Uhh, did you see the weather in Palo Alto tomorrow? "
      "Yeah, can't believe it's gonna rain, dude. Like what?"
    ),
    voice='leah',
    format='mp3',
    return_timestamps=True,
  )
  with open('hello.mp3', 'wb') as f:
    f.write(base64.b64decode(response.audio))
  for chunk in response.timestamps or []:
    print(f'"{chunk.text}" starts at {chunk.start:.3f}s and lasts for {chunk.duration:.3f}s')

asyncio.run(main())
```

```typescript TypeScript
import { writeFileSync } from 'fs';

import Lmnt from 'lmnt-node';

const client = new Lmnt();

const response = await client.speech.generateDetailed({
  text:
    "Uhh, did you see the weather in Palo Alto tomorrow? " +
    "Yeah, can't believe it's gonna rain, dude. Like what?",
  voice: 'leah',
  format: 'mp3',
  return_timestamps: true,
});

writeFileSync('hello.mp3', Buffer.from(response.audio, 'base64'));

for (const chunk of response.timestamps ?? []) {
  console.log(`"${chunk.text}" starts at ${chunk.start.toFixed(3)}s and lasts for ${chunk.duration.toFixed(3)}s`);
}
```

```go Go
package main

import (
	"context"
	"encoding/base64"
	"fmt"
	"log"
	"os"

	"github.com/lmnt-com/lmnt-go/v2"
)

func main() {
	client := lmnt.NewClient()

	response, err := client.Speech.GenerateDetailed(context.TODO(), lmnt.SpeechGenerateDetailedParams{
		Text: "Uhh, did you see the weather in Palo Alto tomorrow? " +
			"Yeah, can't believe it's gonna rain, dude. Like what?",
		Voice:            "leah",
		Format:           lmnt.OutputFormatMP3,
		ReturnTimestamps: lmnt.Bool(true),
	})
	if err != nil {
		log.Fatal(err)
	}

	decoded, err := base64.StdEncoding.DecodeString(response.Audio)
	if err != nil {
		log.Fatal(err)
	}
	if err := os.WriteFile("hello.mp3", decoded, 0o644); err != nil {
		log.Fatal(err)
	}

	for _, chunk := range response.Timestamps {
		fmt.Printf("%q starts at %.3fs and lasts for %.3fs\n",
			chunk.Text, chunk.Start, chunk.Duration)
	}
}
```

```sh cURL
curl --request POST \
  --url https://api.lmnt.com/v1/ai/speech \
  --header "X-API-Key: $LMNT_API_KEY" \
  --header 'lmnt-version: 1.2' \
  --header 'Content-Type: application/json' \
  --data '{
    "text": "Uhh, did you see the weather in Palo Alto tomorrow? Yeah, can'\''t believe it'\''s gonna rain, dude. Like what?",
    "voice": "leah",
    "format": "mp3",
    "return_timestamps": true
  }' \
  | jq -r .audio | base64 -d > hello.mp3
```
</CodeGroup>

<ResponseAudio uuid="0babb5f8-0704-42f5-9884-e72e08ba227b" durations={[
  { text: '', start: 0.000, duration: 0.100 },
  { text: 'Uhh', start: 0.100, duration: 0.140 },
  { text: ', ', start: 0.240, duration: 0.140 },
  { text: 'did', start: 0.380, duration: 0.160 },
  { text: ' ', start: 0.540, duration: 0.020 },
  { text: 'you', start: 0.560, duration: 0.080 },
  { text: ' ', start: 0.640, duration: 0.060 },
  { text: 'see', start: 0.700, duration: 0.160 },
  { text: ' ', start: 0.860, duration: 0.020 },
  { text: 'the', start: 0.880, duration: 0.080 },
  { text: ' ', start: 0.960, duration: 0.060 },
  { text: 'weather', start: 1.020, duration: 0.340 },
  { text: ' ', start: 1.360, duration: 0.120 },
  { text: 'in', start: 1.480, duration: 0.200 },
  { text: ' ', start: 1.680, duration: 0.100 },
  { text: 'Palo', start: 1.780, duration: 0.320 },
  { text: ' ', start: 2.100, duration: 0.080 },
  { text: 'Alto', start: 2.180, duration: 0.260 },
  { text: ' ', start: 2.440, duration: 0.080 },
  { text: 'tomorrow', start: 2.520, duration: 0.460 },
  { text: '? ', start: 2.980, duration: 0.380 },
  { text: 'Yeah', start: 3.360, duration: 0.220 },
  { text: ', ', start: 3.580, duration: 0.080 },
  { text: 'can\'t', start: 3.660, duration: 0.260 },
  { text: ' ', start: 3.920, duration: 0.040 },
  { text: 'believe', start: 3.960, duration: 0.280 },
  { text: ' ', start: 4.240, duration: 0.020 },
  { text: 'it\'s', start: 4.260, duration: 0.100 },
  { text: ' ', start: 4.360, duration: 0.020 },
  { text: 'gonna', start: 4.380, duration: 0.160 },
  { text: ' ', start: 4.540, duration: 0.060 },
  { text: 'rain', start: 4.600, duration: 0.220 },
  { text: ', ', start: 4.820, duration: 0.080 },
  { text: 'dude', start: 4.900, duration: 0.280 },
  { text: '. ', start: 5.180, duration: 0.180 },
  { text: 'Like', start: 5.360, duration: 0.140 },
  { text: ' ', start: 5.500, duration: 0.060 },
  { text: 'what', start: 5.560, duration: 0.320 },
  { text: '?', start: 5.880, duration: 0.060 },
  { text: '', start: 5.940, duration: 0.100 },
]} />

## Getting timestamps with the Speech Sessions API

<Warning>
In the Speech Sessions API, word timestamps currently take longer to arrive
than the generated speech.

The generated speech continues to stream to you in realtime.
</Warning>

<CodeGroup>
```python Python
import asyncio

from lmnt import AsyncLmnt

async def main():
  client = AsyncLmnt()
  session = await client.speech.sessions.create(
    voice='leah',
    return_timestamps=True,
  )
  await session.send_text("Uhh, did you see the weather in Palo Alto tomorrow? ")
  await session.send_text("Yeah, can't believe it's gonna rain, dude. Like what?")
  await session.send_finish()

  with open('hello.mp3', 'wb') as f:
    async for message in session:
      if message.type == 'audio':
        f.write(message.audio)
      elif message.type == 'timestamps':
        for chunk in message.timestamps or []:
          print(f'"{chunk.text}" starts at {chunk.start:.3f}s and lasts for {chunk.duration:.3f}s')

asyncio.run(main())
```

```typescript TypeScript
import { createWriteStream } from 'fs';

import Lmnt from 'lmnt-node';

const client = new Lmnt();
const session = client.speech.sessions.create({
  voice: 'leah',
  return_timestamps: true,
});

await session.sendText("Uhh, did you see the weather in Palo Alto tomorrow? ");
await session.sendText("Yeah, can't believe it's gonna rain, dude. Like what?");
await session.sendFinish();

const audioFile = createWriteStream('hello.mp3');
for await (const message of session) {
  if (message.type === 'audio') {
    audioFile.write(message.audio);
  } else if (message.type === 'timestamps') {
    for (const chunk of message.timestamps ?? []) {
      console.log(`"${chunk.text}" starts at ${chunk.start.toFixed(3)}s and lasts for ${chunk.duration.toFixed(3)}s`);
    }
  }
}
```

```go Go
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/lmnt-com/lmnt-go/v2"
)

func main() {
	ctx := context.TODO()
	client := lmnt.NewClient()

	session, err := client.Speech.Sessions.New(ctx, lmnt.SessionNewParams{
		Voice:            "leah",
		ReturnTimestamps: true,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer session.Close()

	out, err := os.Create("hello.mp3")
	if err != nil {
		log.Fatal(err)
	}
	defer out.Close()

	go func() {
		if err := session.SendText(ctx, "Uhh, did you see the weather in Palo Alto tomorrow? "); err != nil {
			log.Fatal(err)
		}
		if err := session.SendText(ctx, "Yeah, can't believe it's gonna rain, dude. Like what?"); err != nil {
			log.Fatal(err)
		}
		if err := session.SendFinish(ctx); err != nil {
			log.Fatal(err)
		}
	}()

	for msg, err := range session.Messages(ctx) {
		if err != nil {
			log.Fatal(err)
		}
		if audio, ok := msg.(*lmnt.SpeechSessionAudio); ok {
			if _, err := out.Write(audio.Data); err != nil {
				log.Fatal(err)
			}
		}
		if ts, ok := msg.(*lmnt.SpeechSessionTimestamps); ok {
			for _, chunk := range ts.Timestamps {
				fmt.Printf("%q starts at %.3fs and lasts for %.3fs\n",
					chunk.Text, chunk.Start, chunk.Duration)
			}
		}
	}
}
```

```sh websockets
python3 <<'EOF'
import asyncio, json, os, websockets

async def main():
  async with websockets.connect('wss://api.lmnt.com/v1/ai/speech/stream') as ws:
    await ws.send(json.dumps({
      'type': 'init',
      'X-API-Key': os.environ['LMNT_API_KEY'],
      'lmnt-version': '1.2',
      'voice': 'leah',
      'return_timestamps': True,
    }))
    await ws.send(json.dumps({
      'type': 'text',
      'text': "Uhh, did you see the weather in Palo Alto tomorrow? Yeah, can't believe it's gonna rain, dude. Like what?",
    }))
    await ws.send(json.dumps({'type': 'finish'}))
    with open('hello.mp3', 'wb') as f:
      async for msg in ws:
        if isinstance(msg, bytes):
          f.write(msg)
        else:
          data = json.loads(msg)
          if data.get('type') == 'timestamps':
            for ts in data['timestamps']:
              print(f'"{ts["text"]}" starts at {ts["start"]:.3f}s and lasts for {ts["duration"]:.3f}s')

asyncio.run(main())
EOF
```
</CodeGroup>

<ResponseAudio uuid="b8a5514a-1a24-4a2b-a3c7-93196641b99f" durations={[
  { text: '', start: 0.000, duration: 0.060 },
  { text: 'Uhh', start: 0.060, duration: 0.240 },
  { text: ', ', start: 0.300, duration: 0.180 },
  { text: 'did', start: 0.480, duration: 0.140 },
  { text: ' ', start: 0.620, duration: 0.020 },
  { text: 'you', start: 0.640, duration: 0.060 },
  { text: ' ', start: 0.700, duration: 0.080 },
  { text: 'see', start: 0.780, duration: 0.120 },
  { text: ' ', start: 0.900, duration: 0.020 },
  { text: 'the', start: 0.920, duration: 0.060 },
  { text: ' ', start: 0.980, duration: 0.080 },
  { text: 'weather', start: 1.060, duration: 0.240 },
  { text: ' ', start: 1.300, duration: 0.040 },
  { text: 'in', start: 1.340, duration: 0.060 },
  { text: ' ', start: 1.400, duration: 0.060 },
  { text: 'Palo', start: 1.460, duration: 0.280 },
  { text: ' ', start: 1.740, duration: 0.080 },
  { text: 'Alto', start: 1.820, duration: 0.260 },
  { text: ' ', start: 2.080, duration: 0.060 },
  { text: 'tomorrow', start: 2.140, duration: 0.440 },
  { text: '?', start: 2.580, duration: 0.020 },
  { text: '', start: 2.600, duration: 0.433 },
  { text: ' ', start: 3.033, duration: 0.060 },
  { text: 'Yeah', start: 3.093, duration: 0.200 },
  { text: ', ', start: 3.293, duration: 0.100 },
  { text: 'can\'t', start: 3.393, duration: 0.200 },
  { text: ' ', start: 3.593, duration: 0.020 },
  { text: 'believe', start: 3.613, duration: 0.220 },
  { text: ' ', start: 3.833, duration: 0.020 },
  { text: 'it\'s', start: 3.853, duration: 0.080 },
  { text: ' ', start: 3.933, duration: 0.040 },
  { text: 'gonna', start: 3.973, duration: 0.140 },
  { text: ' ', start: 4.113, duration: 0.060 },
  { text: 'rain', start: 4.173, duration: 0.200 },
  { text: ', ', start: 4.373, duration: 0.040 },
  { text: 'dude', start: 4.413, duration: 0.280 },
  { text: '. ', start: 4.693, duration: 0.220 },
  { text: 'Like', start: 4.913, duration: 0.180 },
  { text: ' ', start: 5.093, duration: 0.040 },
  { text: 'what', start: 5.133, duration: 0.280 },
  { text: '?', start: 5.413, duration: 0.060 },
  { text: '', start: 5.473, duration: 0.040 },
]} />
