Metadata-Version: 2.4
Name: srt2speech
Version: 1.5.0
Summary: Synthesize a timestamp-synced speech track from a subtitle file and mux it into video
Author: nbr23
Author-email: nbr23 <max@23.tf>
License-Expression: MIT
License-File: LICENSE
Requires-Dist: pysubs2>=1.7
Requires-Dist: httpx>=0.27
Requires-Dist: typer>=0.12
Requires-Dist: pydub>=0.25
Requires-Dist: rich>=13.7
Requires-Dist: audioop-lts>=0.2 ; python_full_version >= '3.13'
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/nbr23/srt2speech
Project-URL: Repository, https://github.com/nbr23/srt2speech
Description-Content-Type: text/markdown

# srt2speech

Turn a subtitle file into a **timestamp-synced speech track** and mux it into a video.

Give it a video + an `.srt` (or `.vtt`/`.ass`); it synthesizes audio where each subtitle is spoken
at its timestamp, then optionally muxes the track back in with ffmpeg. Useful for restoring lost
audio, rough translation dubs, narrating silent videos, or adding **audio description** by reading
only the descriptive/SDH cues.

It does the SRT→audio part well and nothing else: **no translation, no transcription** — bring an
already-final subtitle file.

## Requirements

- Python ≥ 3.11, [uv](https://docs.astral.sh/uv/)
- `ffmpeg` / `ffprobe` on `PATH`
- A TTS backend:
  - **piper** — a local [gopipertts](https://github.com/nbr23/gopipertts) server (free, default;
    set `SRT2SPEECH_PIPER_URL` if not on `http://localhost:8080`)
  - **openai** — `gpt-4o-mini-tts` (set `OPENAI_API_KEY`)
  - **elevenlabs** — `eleven_multilingual_v2` (set `ELEVENLABS_API_KEY`)

## Install

Run it straight from [PyPI](https://pypi.org/project/srt2speech/) with no install — `uvx` fetches
it on first use:

```sh
uvx srt2speech --help
```

Or install it as a persistent tool (then just call `srt2speech`):

```sh
uv tool install srt2speech
```

## Usage

```sh
# generate a synced track with the local piper backend, sized to the video
uvx srt2speech generate subs.srt --video clip.mp4 -o track.wav

# generate + mux into the video in one step
uvx srt2speech run clip.mp4 subs.srt -o dubbed.mp4

# emit one audio file per segment + a manifest, instead of a merged track
uvx srt2speech generate subs.srt --chunks ./chunks

# raw, per-cue synthesis (no time-fitting)
uvx srt2speech generate subs.srt --chunks ./chunks --chunk-by cue --chunk-audio raw

# paid backend with delivery guidance
OPENAI_API_KEY=... uvx srt2speech generate subs.srt \
    --backend openai --voice coral --instructions "calm documentary narration" -o track.wav

# audio description: only descriptive/SDH cues, mixed over the existing audio
uvx srt2speech run movie.mkv subs.srt --mode descriptive --mux-mode mix -o described.mkv

# mux an existing track yourself
uvx srt2speech mux clip.mp4 track.wav -o dubbed.mp4

# list a backend's voices
uvx srt2speech voices --backend openai
```

### Docker Compose

Runs a local piper server plus an on-demand CLI; no host Python or ffmpeg needed. Put your video
and subtitles in `./data` (mounted at `/data`); pulled voices are cached in `./voices`.

```sh
# 1. start the piper TTS server (preloads the default voice)
docker compose up -d gopipertts

# 2. run the CLI against files in ./data
docker compose run --rm srt2speech run /data/clip.mp4 /data/subs.srt -o /data/dubbed.mp4

# 3. tear down when done
docker compose down
```

For the OpenAI backend, put `OPENAI_API_KEY=sk-...` in a `.env` file (gitignored) — Compose loads it
automatically and passes it through to the CLI container.

### Sync strategies (`--strategy`)

Speech rarely fits a cue's window exactly. The fit engine offers:

- `hybrid` *(default)* — fit into the cue window plus the silent gap before the next cue; only then
  speed up, capped by `--max-speedup` (default `1.15`).
- `overflow` — never speed up; let speech run into following silence (best quality, can drift).
- `precise` — fit the exact cue window, speeding up to the cap.

### Modes (`--mode`)

`all` (default) · `descriptive` (SDH/audio-description only) · `dialogue` (drop sound cues).

### Chunked output (`--chunks`)

Instead of one merged track, `generate --chunks DIR` writes each piece of speech to its own
`.wav` and a `manifest.json` mapping every file back to its timing and text — handy for
re-importing into a video editor. In this mode `-o`/`--video` are ignored.

- `--chunk-by` — `segment` *(default)*: merged sentence-sized units (better prosody); `cue`:
  one file per raw subtitle entry.
- `--chunk-audio` — `fitted` *(default)*: time-shaped to its window per `--strategy`; `raw`:
  the natural synthesis with no time-stretching.

Files are named `<index>_<start_ms>ms.wav` (e.g. `0003_0012400ms.wav`). The manifest records
`start_ms`/`end_ms` (the cue/segment window), `audio_ms` (rendered length), and `text` per chunk.

## Development

From a clone of the repo:

```sh
uv sync
uv run srt2speech --help
uv run pytest
uv run ruff check
```
