cuewright
Transcribe speech, translate it, and produce subtitles you can actually trust.
Built around one idea: timing and wording live in separate files. Transcription establishes timestamps once. Every language track then supplies only text, keyed by segment id. Editing a translation costs nothing to re-render, and all your subtitle tracks stay frame-accurate with each other because none of them invents its own timing.
The other half of the project is verification. Machine transcription fails quietly —
a dropped sentence or a misheard word looks exactly like a correct one until someone
watches the whole video. cuewright ships the checks that narrow down where to look.
Install
pip install cuewright
Requires Python 3.10+ and ffmpeg on PATH.
Windows : winget install Gyan.FFmpeg
macOS : brew install ffmpeg
Debian : sudo apt install ffmpeg
If ffmpeg lives somewhere unusual, point at it with CUEWRIGHT_FFMPEG (and
CUEWRIGHT_FFPROBE). A static build unpacked into tools/ is also found automatically.
Quick start
Turning an Arabic-language video into Turkish subtitles:
# 1. Transcribe. Writes video.ar.srt and video.ar.segments.json
cuewright transcribe video.mp4 --language ar
# 2. Emit a translation skeleton, one "[id] text" line per segment
cuewright template video.ar.segments.json -o video.tr.txt --with-source
# 3. Translate the text in video.tr.txt however you like, keeping the [id] markers
# 4. Build the subtitle file — timings come from the segments, text from your file
cuewright build video.ar.segments.json video.tr.txt video.tr.srt
# 5. Optionally render it into the picture
cuewright burn video.mp4 video.tr.srt video.tr.mp4
Step 4 refuses to write anything if the ids do not line up exactly. That is deliberate: one skipped line would slide every later cue onto the wrong shot, which is far harder to spot than an error at build time.
For subtitles in the source language, skip steps 2–4:
cuewright export video.ar.segments.json video.ar.vtt
Verifying the result
Machine transcription goes wrong in two distinct ways, and they need different checks.
Dropped speech. Voice-activity detection sometimes mistakes quiet speech for
silence. verify finds every stretch the segments do not cover and measures its
audio level — a gap carrying speech-level sound is worth listening to.
cuewright verify video.ar.segments.json video.mp4
49 segments | speech 242s / 251s (96.5%)
coverage gaps (2)
reference speech level: -17.8 dB
0.00s - 5.87s ( 5.87s) | mean -22.3 dB, peak -7.2 dB | before segment 1 <-- speech-level audio, check this
247.76s - 250.63s ( 2.87s) | mean -32.0 dB, peak -19.2 dB | end of media
Misheard words. Decode the same audio a second time with different settings, then compare. Where the passes agree the transcript is dependable; where they diverge the model was unsure.
cuewright transcribe video.mp4 --language ar --no-vad --tag check
cuewright compare video.ar.segments.json video.check.segments.json
word-level agreement: 99.76%
[replace] … توفر رمال … المحلية عالية الجودة …
A: السلك
B: السيليكا
That single disagreement was a real transcription error, found without anyone listening to the audio.
Be clear about the limit: both passes usually share a model, so a mistake the model makes consistently survives this check. It finds uncertainty, not truth. Proper nouns and numbers still deserve a human ear.
Finally, validate the subtitle file itself — numbering, overlaps, line lengths:
cuewright check video.tr.srt
Subtitle formatting
Cues are wrapped to two lines of ~42 characters. Segments longer than that are split
into several cues with time apportioned by length, preferring sentence and clause
boundaries. When no punctuation is available the splitter scores candidate positions
so it will not separate a number from its unit (400 | metre) or strand a
conjunction at the end of a block.
Using it as a library
from cuewright import SegmentFile, transcribe, verify, write_srt
from cuewright.transcribe import TranscribeOptions
result = transcribe("video.mp4", TranscribeOptions(model="large-v3", language="ar"))
result.save("video.segments.json")
write_srt(result.segments, "video.ar.srt")
report = verify(result, "video.mp4")
print(f"coverage {report.coverage:.1%}, {len(report.anomalies)} anomalies")
Commands
| Command | Purpose |
|---|---|
transcribe |
Speech to timestamped segments and an SRT |
template |
Translation skeleton ([id] text per segment) |
build |
Segments plus a translation to SRT or VTT |
export |
Segments to SRT or VTT in the source language |
burn |
Render subtitles into the picture |
verify |
Find dropped speech and implausible segments |
compare |
Cross-check two transcription passes |
check |
Validate SRT structure |
Run cuewright <command> --help for the full set of options.
Notes on quality
- Transcription runs with
condition_on_previous_text=False. Feeding previous text back in makes the model prone to repetition loops; losing a little cross-segment context is the cheaper trade. burncopies the audio stream untouched and re-encodes only the video, because drawing text necessarily changes the picture.- Subtitles are resolution-independent. If you later obtain a higher-resolution
master, re-run
burnwith a proportionally larger--font-size— there is no need to transcribe again.
Development
git clone https://github.com/habibkaratas/cuewright
cd cuewright
pip install -e ".[dev]"
pytest
ruff check .
The test suite runs without ffmpeg or model weights: the structural half of every check is unit-testable on its own.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cuewright-0.1.1.tar.gz.
File metadata
- Download URL: cuewright-0.1.1.tar.gz
- Upload date:
- Size: 32.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
444827ceb53401dc88701b7b94c9b4aaebe0fa304867c40daa5bf14d7cfe519a
|
|
| MD5 |
275e60f121774e27dc0d5b807415d639
|
|
| BLAKE2b-256 |
ac1801b5c3bf1cf285d20e343838888610a65091ba95a2def72d0732a78a6178
|
Provenance
The following attestation bundles were made for cuewright-0.1.1.tar.gz:
Publisher:
release.yml on habibkaratas/cuewright
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cuewright-0.1.1.tar.gz -
Subject digest:
444827ceb53401dc88701b7b94c9b4aaebe0fa304867c40daa5bf14d7cfe519a - Sigstore transparency entry: 2249881736
- Sigstore integration time:
-
Permalink:
habibkaratas/cuewright@b96ae74ebdb2fd38d92a9be4ae5adc5cf0844e06 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/habibkaratas
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b96ae74ebdb2fd38d92a9be4ae5adc5cf0844e06 -
Trigger Event:
push
-
Statement type:
File details
Details for the file cuewright-0.1.1-py3-none-any.whl.
File metadata
- Download URL: cuewright-0.1.1-py3-none-any.whl
- Upload date:
- Size: 23.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae72cee5d6d0d21a715b4e74a7abb74f54b27c0fdbdd6794676b0ddee33b8bc7
|
|
| MD5 |
35956269274a00c6aa6e3095f3c7a0e9
|
|
| BLAKE2b-256 |
caef23afb422e950cc3dd51ac749536b3acd1e3971a389a63fec7a1b3b35bdcb
|
Provenance
The following attestation bundles were made for cuewright-0.1.1-py3-none-any.whl:
Publisher:
release.yml on habibkaratas/cuewright
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cuewright-0.1.1-py3-none-any.whl -
Subject digest:
ae72cee5d6d0d21a715b4e74a7abb74f54b27c0fdbdd6794676b0ddee33b8bc7 - Sigstore transparency entry: 2249882427
- Sigstore integration time:
-
Permalink:
habibkaratas/cuewright@b96ae74ebdb2fd38d92a9be4ae5adc5cf0844e06 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/habibkaratas
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b96ae74ebdb2fd38d92a9be4ae5adc5cf0844e06 -
Trigger Event:
push
-
Statement type: