# ChatGPT API SRT Subtitle Translator Large language models (LLMs), such as ChatGPT, have demonstrated their capabilities as a [robust translator](https://arxiv.org/abs/2303.13780), capable of handling common natural languages with contextual understanding, as well as unconventional forms of writing like [word scrambling](https://www.mrc-cbu.cam.ac.uk/people/matt.davis/cmabridge/). However, it may not always produce a deterministic output or adhere to line-to-line correlation, potentially disrupting the timing of subtitles, even when instructed to follow precise instructions and with the model `temperature` parameter set to [`0`](https://cobusgreyling.medium.com/example-code-implementation-considerations-for-gpt-3-5-turbo-chatml-whisper-e61f8703c5db). This utility uses the OpenAI ChatGPT API to translate text, with a specific focus on line-based translation, especially for SRT subtitles. The translator optimizes token usage by removing SRT overhead and grouping text into batches, resulting in arbitrary length translations without excessive [token consumption](https://openai.com/api/pricing/) while ensuring a one-to-one match between line input and output. ### Upgrading from v2? See the [v2 -> v3 Migration Guide](docs/CHANGELOG.md#300-2026-03-01) for breaking changes. ## Web Interface: ## Features - Web User Interface (Web UI) and Command Line Interface (CLI) - Supports [Structured Output](https://openai.com/index/introducing-structured-outputs-in-the-api/): for more concise results, enabled by default in the Web UI and CLI - Supports [Prompt Caching](https://openai.com/index/api-prompt-caching/): by including the full context of translated data, the system instruction and translation context are packaged to work well with prompt caching, controlled with `-c, --context` (CLI only) - Supports any OpenAI API compatible providers such as running [Ollama](https://ollama.com/) locally - Line-based batching: avoids token limit per request, reduces overhead token wastage, and maintains translation context to a certain extent - Optional OpenAI Moderation tool check: prevents token wastage if the model is highly likely to refuse to translate, enabled with `--use-moderator` (CLI only) - Streaming process output - Request per minute (RPM) [rate limits](https://platform.openai.com/docs/guides/rate-limits/overview) - Progress resumption (CLI only) ## Setup Reference: - Node.js version `>= 20` required. This README assumes `bash` shell environment - Clone this repository ```bash git clone https://github.com/Cerlancism/chatgpt-subtitle-translator ``` - Navigate into the directory ```bash cd chatgpt-subtitle-translator ``` - Install the requirements ```bash npm install ``` - Give executable permission ```bash chmod +x cli/translator.mjs ``` - Copy `.example.env` to `.env` ```bash cp .env.example .env ``` - Add your [API key](https://platform.openai.com/account/api-keys) to the newly created `.env` file ## CLI ```bash cli/translator.mjs --help ``` `Usage: translator [options]` `Translation tool based on ChatGPT API` Options: - `--from ` Source language (default: "") - `--to ` Target language (default: "English") - `-s, --system-instruction ` Override the prompt system instruction template `Translate ${from} to ${to}` with this text, **ignoring `--from` and `--to` options** - `-i, --input ` Input source text with the content of this file, in `.srt` format or plain text - `-o, --output ` Output file name, defaults to be based on input file name - `-r, --structured ` [Structured response](https://openai.com/index/introducing-structured-outputs-in-the-api/) format mode. (default: `array`, choices: `array`, `object`, `timestamp`, `agent`, `none`) - `array` Structures the input and output into an array format. - `object` Structures the input and output as a keyed object. - `timestamp` Provides the model with entry timings alongside each entry's text (a batch start offset plus per-entry offset/length in milliseconds), allowing it to merge adjacent entries into one. A batch is only retried when the output time span boundaries don't match the input - unlike other modes which retry on any line count mismatch - significantly reducing token wastage from retries. Uses more tokens per batch due to timestamps in input and output. Output entry count may differ from input, so progress file resumption is not supported. - `agent` Alias for the `agent` subcommand with default options. Use the dedicated subcommand for full configuration. See [Agent Mode](#agent-mode). - `none` Legacy compatibility mode, disables structured output. - `-c, --context ` Include translation history up to a token budget to work well with [prompt caching](https://openai.com/index/api-prompt-caching/). Default: `2000`. Set to `0` to include history without a token limit check. The token budget is tracked from actual model response token counts. The history is chunked into user/assistant message pairs using the last value in `--batch-sizes`. Recommended value: set `` up to ~30% less than the model's max context length to leave room for the current batch and system prompts. For example, for a `128K` context model: `--context 90000`. - `-b, --batch-sizes ` Batch sizes of increasing order for translation prompt slices in JSON Array The number of lines to include in each translation prompt, provided that they are estimated to be within the token limit. In case of mismatched output line quantities, this number will be decreased step by step according to the values in the array, ultimately reaching one. Larger batch sizes generally lead to more efficient token utilization and potentially better contextual translation. However, mismatched output line quantities or exceeding the token limit will cause token wastage, requiring resubmission of the batch with a smaller batch size. When omitted, batch size is determined automatically per batch based on the `--context` token budget. On failure, the size is reduced and retried down to a minimum, then resets on the next successful batch. - `-g, --guard-repetition ` Minimum number of pattern repeats before aborting a streaming response (default: `10`). When the model falls into a repetition loop during streaming, the response is aborted and retried with a smaller batch. Set to `0` to disable repetition detection. - `--initial-prompts ` Initial prompts for the translation in JSON (default: `"[]"`) - `--use-moderator` Use the OpenAI Moderation tool - `--moderation-model ` (default: `"omni-moderation-latest"`) https://developers.openai.com/api/docs/models - `--no-prefix-number` Don't prefix lines with numerical indices. Ignored in `-r, --structured` `array|object|timestamp` - prefix numbers are always disabled there. - `--no-line-matching` Don't enforce one-to-one line quantity input output matching. Ignored in `-r, --structured` `timestamp` - line matching is always disabled there since entries may be merged. - `-p, --plain-text ` Input source text with this plain text argument. Not supported in `-r, --structured` `timestamp` mode, or when using the `agent` subcommand with `-r timestamp`. - `--no-stream` Disable stream progress output to terminal (streaming is on by default) - `--log-level ` Log level (default: `debug`, choices: `trace`, `debug`, `info`, `warn`, `error`, `silent`) - `--silent` Same as `--log-level silent` - `--quiet` Same as `--log-level silent` Additional Options for GPT: https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create - `-m, --model ` (default: `"gpt-4o-mini"`) https://developers.openai.com/api/docs/models - `--reasoning_effort ` Constrains effort on reasoning for reasoning models. Accepted values depend on the model (e.g. `"low"`, `"medium"`, `"high"`), follows the model's default when not set. `"none"` disables reasoning/thinking entirely (supported by OpenAI o-series/GPT-5+ and open models via Ollama such as Qwen3). - `-t, --temperature ` Sampling temperature to use, should set a low value such as `0` to be more deterministic for translation (default: `0`) - `--top_p ` Nucleus sampling parameter, top_p probability mass - `--presence_penalty ` Penalty for new tokens based on their presence in the text so far - `--frequency_penalty ` Penalty for new tokens based on their frequency in the text so far - `--logit_bias ` Modify the likelihood of specified tokens appearing in the completion ### Agent Mode Subcommand for multi-pass agentic translation. Accepts all standard translation options. ```bash cli/translator.mjs agent --help ``` Agent mode runs multiple passes before translating: **Overview** - Samples the file to produce a content overview (file identity, duration, genre/tone, character names) and detects the source language. **Planning** - Scans the file in token-bounded windows. Each window produces a batch summary (characters, locations, events, tone). Summaries are consolidated and used to generate a refined translation instruction. **Translation** - Translates using the enriched instruction. After the first batch, a sample of the output is checked to confirm the target language before proceeding. Structured mode defaults to `array`, pass `--structured timestamp` to use timestamp mode instead. ```bash # Default (array delegate) cli/translator.mjs agent --input subtitles.srt --from Japanese --to English # Timestamp delegate cli/translator.mjs agent --input subtitles.srt --structured timestamp --from Japanese --to English ``` - `--skip-refine` Skip the final instruction refinement step at the end of the planning pass and use the original system instruction directly. - `--no-fitting` Skip LLM-based token-range fitting for planning summaries and consolidation. Summaries are used as-is regardless of token range. - `--context-summary ` Provide a context summary directly, bypassing the planning pass entirely and proceeding straight to translation. ## Examples ### Plain text ```bash cli/translator.mjs --plain-text "δ½ ε₯½" ``` Standard Output ``` Hello. ``` ### Emojis ```bash cli/translator.mjs --to "Emojis" --plain-text "Chuck Norris can walk with the animals, talk with the animals; grunt and squeak and squawk with the animals... and the animals, without fail, always say 'yessir Mr. Norris'." ``` Standard Output ``` πŸ‘¨β€πŸ¦°πŸ’ͺπŸšΆβ€β™‚οΈπŸ¦œπŸ’πŸ˜πŸ…πŸ†πŸŽπŸ–πŸ„πŸ‘πŸ¦πŸŠπŸ’πŸπŸΏοΈπŸ‡πŸΏοΈβ—οΈπŸŒ³πŸ’¬πŸ˜²πŸ‘‰πŸ€΅πŸ‘¨β€πŸ¦°πŸ‘Š=πŸ•πŸ‘πŸπŸ¦ŒπŸ˜πŸ¦πŸ¦πŸ¦§πŸ¦“πŸ…πŸ¦ŒπŸ¦ŒπŸ¦ŒπŸ†πŸ¦πŸ˜πŸ˜πŸ—πŸ¦“=πŸ‘πŸ€΅. ``` ### Scrambling ```bash cli/translator.mjs --system-instruction "Scramble characters of words while only keeping the start and end letter" --no-prefix-number --no-line-matching --plain-text "Chuck Norris can walk with the animals, talk with the animals;" ``` Standard Output ``` Cuhck Nroris can wakl wtih the aiamnls, talk wtih the aiamnls; ``` ### Unscrambling ```bash cli/translator.mjs --system-instruction "Unscramble characters back to English" --no-prefix-number --no-line-matching --plain-text "Cuhck Nroris can wakl wtih the aiamnls, talk wtih the aiamnls;" ``` Standard Output ``` Chuck Norris can walk with the animals, talk with the animals; ``` ### Plain text file ```bash cli/translator.mjs --input test/data/test_cn.txt ``` Input file: [test/data/test_cn.txt](test/data/test_cn.txt) ``` δ½ ε₯½γ€‚ ζ‹œζ‹œοΌ ``` Standard Output ``` Hello. Goodbye! ``` ### SRT file ```bash cli/translator.mjs --input test/data/test_ja_small.srt ``` Input file: [test/data/test_ja_small.srt](test/data/test_ja_small.srt) ```srt 1 00:00:00,000 --> 00:00:02,000 γŠγ―γ‚ˆγ†γ”γ–γ„γΎγ™γ€‚ 2 00:00:02,000 --> 00:00:05,000 γŠε…ƒζ°—γ§γ™γ‹οΌŸ 3 00:00:05,000 --> 00:00:07,000 はい、元気です。 4 00:00:08,000 --> 00:00:12,000 今ζ—₯γ―ε€©ζ°—γŒγ„γ„γ§γ™γ­γ€‚ 5 00:00:12,000 --> 00:00:16,000 はい、とてもいい倩気です。 ``` Output file: [test/data/test_ja_small.srt.out_English.srt](test/data/test_ja_small.srt.out_English.srt) ```srt 1 00:00:00,000 --> 00:00:02,000 Good morning. 2 00:00:02,000 --> 00:00:05,000 How are you? 3 00:00:05,000 --> 00:00:07,000 Yes, I'm doing well. 4 00:00:08,000 --> 00:00:12,000 The weather is nice today, isn't it? 5 00:00:12,000 --> 00:00:16,000 Yes, it's very nice weather. ``` ## How it works SRT indices and timestamps are stripped or simplified before sending to the model, reducing tokens. Lines are batched together into a single prompt - removing repeated per-entry overhead. The default system instruction is a minimal `Translate to ` (3 tokens). Structured output modes enforce a schema so the model returns only the translated text. Five modes are available via `--structured`: #### `array` (default) Lines are sent as a JSON array. The model returns a matching array.
Input (SRT) Prompt (User Message) Transform (Model Response) Output (SRT)
Tokens: `139` Tokens: `52` Tokens: `38` Tokens: `127`
```srt 1 00:00:00,000 --> 00:00:02,000 γŠγ―γ‚ˆγ†γ”γ–γ„γΎγ™γ€‚ 2 00:00:02,000 --> 00:00:05,000 γŠε…ƒζ°—γ§γ™γ‹οΌŸ 3 00:00:05,000 --> 00:00:07,000 はい、元気です。 4 00:00:08,000 --> 00:00:12,000 今ζ—₯γ―ε€©ζ°—γŒγ„γ„γ§γ™γ­γ€‚ 5 00:00:12,000 --> 00:00:16,000 はい、とてもいい倩気です。 ``` *(compact JSON, formatted here for readability)* ```json { "inputs": [ "γŠγ―γ‚ˆγ†γ”γ–γ„γΎγ™γ€‚", "γŠε…ƒζ°—γ§γ™γ‹οΌŸ", "はい、元気です。", "今ζ—₯γ―ε€©ζ°—γŒγ„γ„γ§γ™γ­γ€‚", "はい、とてもいい倩気です。" ] } ``` *(compact JSON, formatted here for readability)* ```json { "outputs": [ "Good morning.", "How are you?", "Yes, I'm doing well.", "The weather is nice today, isn't it?", "Yes, it's very nice weather." ] } ``` ```srt 1 00:00:00,000 --> 00:00:02,000 Good morning. 2 00:00:02,000 --> 00:00:05,000 How are you? 3 00:00:05,000 --> 00:00:07,000 Yes, I'm doing well. 4 00:00:08,000 --> 00:00:12,000 The weather is nice today, isn't it? 5 00:00:12,000 --> 00:00:16,000 Yes, it's very nice weather. ```
#### `object` Source lines are used as keys in the response schema. The model maps each source key to its translation. No explicit user message is sent - the schema itself conveys the input.
Input (SRT) Prompt (Schema Keys) Transform (Model Response) Output (SRT)
Tokens: `139` Tokens: `~60` Tokens: `85` Tokens: `127`
```srt 1 00:00:00,000 --> 00:00:02,000 γŠγ―γ‚ˆγ†γ”γ–γ„γΎγ™γ€‚ 2 00:00:02,000 --> 00:00:05,000 γŠε…ƒζ°—γ§γ™γ‹οΌŸ 3 00:00:05,000 --> 00:00:07,000 はい、元気です。 4 00:00:08,000 --> 00:00:12,000 今ζ—₯γ―ε€©ζ°—γŒγ„γ„γ§γ™γ­γ€‚ 5 00:00:12,000 --> 00:00:16,000 はい、とてもいい倩気です。 ``` *Source lines are encoded as response schema keys (no user message)* ```json { "γŠγ―γ‚ˆγ†γ”γ–γ„γΎγ™γ€‚": "string", "γŠε…ƒζ°—γ§γ™γ‹οΌŸ": "string", "はい、元気です。": "string", "今ζ—₯γ―ε€©ζ°—γŒγ„γ„γ§γ™γ­γ€‚": "string", "はい、とてもいい倩気です。": "string" } ``` *(compact JSON, formatted here for readability)* ```json { "γŠγ―γ‚ˆγ†γ”γ–γ„γΎγ™γ€‚": "Good morning.", "γŠε…ƒζ°—γ§γ™γ‹οΌŸ": "How are you?", "はい、元気です。": "Yes, I'm doing well.", "今ζ—₯γ―ε€©ζ°—γŒγ„γ„γ§γ™γ­γ€‚": "The weather is nice today, isn't it?", "はい、とてもいい倩気です。": "Yes, it's very nice weather." } ``` ```srt 1 00:00:00,000 --> 00:00:02,000 Good morning. 2 00:00:02,000 --> 00:00:05,000 How are you? 3 00:00:05,000 --> 00:00:07,000 Yes, I'm doing well. 4 00:00:08,000 --> 00:00:12,000 The weather is nice today, isn't it? 5 00:00:12,000 --> 00:00:16,000 Yes, it's very nice weather. ```
#### `timestamp` Timestamps are preserved alongside the text. Lines are sent using the compact [Toon format](https://www.npmjs.com/package/@toon-format/toon): each batch carries its absolute start time as a top-level `offset`, and each entry an `offset` (relative to the batch start) and `length` (duration) in milliseconds. This keeps the numbers small regardless of the batch's position in the file - absolute timestamps grow to 7 digits over a feature-length runtime. The model may merge subtitle entries when contextually appropriate.
Input (SRT) Prompt (User Message) Transform (Model Response) Output (SRT)
Tokens: `139` Tokens: `97` Tokens: `100` Tokens: `127`
```srt 1 00:00:00,000 --> 00:00:02,000 γŠγ―γ‚ˆγ†γ”γ–γ„γΎγ™γ€‚ 2 00:00:02,000 --> 00:00:05,000 γŠε…ƒζ°—γ§γ™γ‹οΌŸ 3 00:00:05,000 --> 00:00:07,000 はい、元気です。 4 00:00:08,000 --> 00:00:12,000 今ζ—₯γ―ε€©ζ°—γŒγ„γ„γ§γ™γ­γ€‚ 5 00:00:12,000 --> 00:00:16,000 はい、とてもいい倩気です。 ``` *(Toon format - compact, not JSON)* ```yaml offset: 0 inputs[5]{offset,length,text}: 0,2000,γŠγ―γ‚ˆγ†γ”γ–γ„γΎγ™γ€‚ 2000,3000,γŠε…ƒζ°—γ§γ™γ‹οΌŸ 5000,2000,はい、元気です。 8000,4000,今ζ—₯γ―ε€©ζ°—γŒγ„γ„γ§γ™γ­γ€‚ 12000,4000,はい、とてもいい倩気です。 ``` *(compact JSON, formatted here for readability)* ```json { "outputs": [ { "offset": 0, "length": 2000, "text": "Good morning." }, { "offset": 2000, "length": 3000, "text": "How are you?" }, { "offset": 5000, "length": 2000, "text": "Yes, I'm doing well." }, { "offset": 8000, "length": 4000, "text": "The weather is nice today, isn't it?" }, { "offset": 12000, "length": 4000, "text": "Yes, it's very nice weather." } ] } ``` ```srt 1 00:00:00,000 --> 00:00:02,000 Good morning. 2 00:00:02,000 --> 00:00:05,000 How are you? 3 00:00:05,000 --> 00:00:07,000 Yes, I'm doing well. 4 00:00:08,000 --> 00:00:12,000 The weather is nice today, isn't it? 5 00:00:12,000 --> 00:00:16,000 Yes, it's very nice weather. ```
#### `none` No structured output. Lines are sent as text and the model returns text.
Input (SRT) Prompt (User Message) Transform (Model Response) Output (SRT)
Tokens: `139` Tokens: `59` Tokens: `42` Tokens: `127`
```srt 1 00:00:00,000 --> 00:00:02,000 γŠγ―γ‚ˆγ†γ”γ–γ„γΎγ™γ€‚ 2 00:00:02,000 --> 00:00:05,000 γŠε…ƒζ°—γ§γ™γ‹οΌŸ 3 00:00:05,000 --> 00:00:07,000 はい、元気です。 4 00:00:08,000 --> 00:00:12,000 今ζ—₯γ―ε€©ζ°—γŒγ„γ„γ§γ™γ­γ€‚ 5 00:00:12,000 --> 00:00:16,000 はい、とてもいい倩気です。 ``` ``` 1. γŠγ―γ‚ˆγ†γ”γ–γ„γΎγ™γ€‚ 2. γŠε…ƒζ°—γ§γ™γ‹οΌŸ 3. はい、元気です。 4. 今ζ—₯γ―ε€©ζ°—γŒγ„γ„γ§γ™γ­γ€‚ 5. はい、とてもいい倩気です。 ``` ``` 1. Good morning. 2. How are you? 3. Yes, I'm doing well. 4. The weather is nice today, isn't it? 5. Yes, it's very nice weather. ``` ```srt 1 00:00:00,000 --> 00:00:02,000 Good morning. 2 00:00:02,000 --> 00:00:05,000 How are you? 3 00:00:05,000 --> 00:00:07,000 Yes, I'm doing well. 4 00:00:08,000 --> 00:00:12,000 The weather is nice today, isn't it? 5 00:00:12,000 --> 00:00:16,000 Yes, it's very nice weather. ```