# Thai subtitles from word timestamps

A Thai sentence has no spaces to cut a subtitle at. How word timestamps give a cue its cut points, the rules a Thai cue file needs, and the convention to ask for.

Published 2026-09-09. Paxa Labs.

A subtitle is a transcript cut into pieces that fit on a screen for the seconds they are spoken. In English the cutting is easy. A cue ends at a space, and a tool that knows nothing about the language produces readable lines. Thai writes a whole phrase as one run of characters. A tool that cuts at a character count cuts through a word, and the viewer reads half a word on one line and the other half two seconds later.

This post shows how to build Thai cues from the word timestamps Paxa STT Lite returns. Each span is a whole Thai word with its start and end, which is exactly the set of places a cue may be cut. The rules for turning spans into cues fit on one page, and the result is a subtitle file any player reads.

## What a span is

Ask for `timestamps` set to `"word"` and the response carries a `words` array beside the transcript. Each entry is one word with its start and end in seconds. For Thai the words are segmented on the way out. A span covers a whole word and never a character. The spans join back into the transcript with nothing lost.

curl:

```bash
AUDIO=$(base64 < meeting.m4a | tr -d '\n')
curl -X POST https://api.paxalabs.com/v1/stt \
  --max-time 300 \
  -H "Authorization: Bearer $PAXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"timestamps\": \"word\"}"
```

Response:

```json
{
  "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ",
  "words": [
    {
      "text": "สวัสดี",
      "start": 0.12,
      "end": 0.44
    },
    {
      "text": "ค่ะ",
      "start": 0.52,
      "end": 0.66
    },
    {
      "text": "คุณ",
      "start": 1.08,
      "end": 1.24
    },
    {
      "text": "สมชาย",
      "start": 1.32,
      "end": 1.88
    },
    {
      "text": "ยอด",
      "start": 2.36,
      "end": 2.56
    },
    {
      "text": "ชำระ",
      "start": 2.6,
      "end": 2.879
    },
    {
      "text": "ของ",
      "start": 3,
      "end": 3.14
    },
    {
      "text": "ท่าน",
      "start": 3.2,
      "end": 3.359
    },
    {
      "text": "จำนวน",
      "start": 3.48,
      "end": 3.879
    },
    {
      "text": "พันสองร้อยห้าสิบ",
      "start": 4.24,
      "end": 5.4
    },
    {
      "text": "บาท",
      "start": 5.56,
      "end": 5.759
    },
    {
      "text": "ครบ",
      "start": 6.32,
      "end": 6.52
    },
    {
      "text": "กำหนด",
      "start": 6.56,
      "end": 6.84
    },
    {
      "text": "วัน",
      "start": 6.92,
      "end": 7.039
    },
    {
      "text": "ที่",
      "start": 7.04,
      "end": 7.119
    },
    {
      "text": "สิบห้า",
      "start": 7.24,
      "end": 7.48
    },
    {
      "text": "สิงหาคม",
      "start": 7.68,
      "end": 8.26
    },
    {
      "text": "นี้",
      "start": 8.36,
      "end": 8.5
    },
    {
      "text": "ชำระ",
      "start": 9.08,
      "end": 9.36
    },
    {
      "text": "ผ่าน",
      "start": 9.48,
      "end": 9.68
    },
    {
      "text": "แอป",
      "start": 9.76,
      "end": 9.92
    },
    {
      "text": "ได้",
      "start": 10,
      "end": 10.14
    },
    {
      "text": "ตลอด",
      "start": 10.2,
      "end": 10.46
    },
    {
      "text": "ยี่สิบสี่",
      "start": 10.56,
      "end": 10.96
    },
    {
      "text": "ชั่วโมง",
      "start": 11.08,
      "end": 11.4
    },
    {
      "text": "ค่ะ",
      "start": 11.52,
      "end": 11.66
    }
  ],
  "usage": {
    "seconds": 11.952,
    "credits": 1.66
  }
}
```

## Cutting spans into cues

Walk the spans in order and collect them into a cue until one of three limits is reached. The cue then ends at the last span's end, and the next cue starts at the next span's start.

- Length. A Thai line reads comfortably at around thirty-five characters, and a cue holds at most two lines. Count the characters of the collected words and cut before the word that would push past the limit.
- Duration. A cue on screen for longer than about six seconds reads as stuck. Cut when the next word would push the cue past the limit, even when the line has room.
- Pause. A gap between one span's end and the next span's start longer than about half a second is a breath or a full stop. Cut there, and the cue ends when the speaker does.

A cue that ends at a pause reads better than one that ends at a count. A small overrun of the length limit to reach a pause is worth allowing. Where a cue has two lines, break the line at the widest gap between spans inside the cue, which is usually a phrase boundary.

one sentence, two cues:

```
words
  วันนี้      0.00  0.38
  เรา         0.38  0.55
  จะ          0.55  0.70
  คุย         0.70  0.98
  เรื่อง      0.98  1.30
  งบประมาณ    1.30  1.92
  ปีหน้า      1.92  2.40
  กัน         2.40  2.61
  นะครับ      2.61  3.05

cues
  1  00:00:00,000 --> 00:00:01,300
     วันนี้เราจะคุยเรื่อง
  2  00:00:01,300 --> 00:00:03,050
     งบประมาณปีหน้ากันนะครับ
```

## The convention to ask for

Subtitles want the spoken convention, which is the default. The viewer hears สองพันสามร้อยห้าสิบบาท and reads the same words. A digit on screen while the speaker says seven syllables reads as a mismatch, and a cue built from the written form also loses the span boundaries inside a number, since the digits arrive as one token.

Keep the verbatim style for the same reason. A viewer who hears a false start and reads a cleaned sentence notices. The clean style is for a transcript read on its own, away from the audio.

## Writing the file

SRT numbers each cue, writes the start and end as hours, minutes, seconds, and milliseconds with a comma before the milliseconds, and follows with the text. WebVTT is the same with a period before the milliseconds and a header line. Both are plain text, and both carry Thai as UTF-8 with no further setting. Write the seconds from the spans with three decimals and the file is done.

For a translated track, send the cues' text to the translation API one cue per segment with the ids echoed back. The timings stay with the ids. A Thai video gets an English track with the same cut points, and the two tracks stay aligned.

## Next steps

- Read the [transcription guide](https://paxalabs.com/docs/speech-to-text#timestamps) for the word timestamps beside the model's own spans on one recording.
- Read the [timestamps page](https://paxalabs.com/docs/timestamps) for the span shape shared with speech synthesis, where the same rules build read-along captions.
- Read the [numbers post](https://paxalabs.com/guides/thai-transcript-numbers-spoken-or-written) for why a subtitle keeps the spoken form.
- Read the [captions use case](https://paxalabs.com/use-cases/captions) for a caption pipeline end to end and what a season of video costs.
