A production-safe Python library for reading, editing, transforming, and saving .srt subtitle files.
Project description
SRTManager — Complete Documentation
A production-safe Python library for reading, editing, transforming, and saving
.srtsubtitle files.
Table of Contents
- Installation
- Quick Start
- Core Concepts
- Creating a Manager
- Properties
- Shifting & Timing
- Slicing
- Merging
- Searching
- Editing Content
- Splitting
- Gap Compression
- Retime / Remove / Insert
- Diffing
- Exporting & Saving
- Mutation: add_raw
- Error Reference
- Full Workflow Examples
Installation
pip install srtmanager
from srtmanager import SRTManager, SRTValidationError
Quick Start
from srtmanager import SRTManager
# Load a file
mgr = SRTManager.from_file("movie.srt")
# Shift all subtitles 2.5 seconds later
mgr2 = mgr >> 2.5
# Extract a 60-second clip starting at t=120s
clip = mgr.slice(120, 180)
# Save it
clip.save("clip.srt")
Core Concepts
Immutability by default.
Every transformation method returns a new SRTManager. The original is never changed. The only exception is add_raw(), which is explicitly documented as mutating.
Invariants enforced automatically. On every construction the library:
- Sorts subtitles by start time
- Reindexes from 1
- Validates no negative timestamps
- Validates no overlapping subtitles
- Strips leading/trailing whitespace from content
If any invariant is violated, SRTValidationError is raised immediately.
Creating a Manager
From a file
mgr = SRTManager.from_file("subtitles.srt")
# Non-UTF-8 files (common on Windows)
mgr = SRTManager.from_file("subtitles.srt", encoding="latin-1")
mgr = SRTManager.from_file("subtitles.srt", encoding="cp1252")
From a string
raw = """
1
00:00:01,000 --> 00:00:03,000
Hello world
2
00:00:04,000 --> 00:00:06,000
How are you?
"""
mgr = SRTManager.from_string(raw)
From a list of srt.Subtitle objects
import srt
from datetime import timedelta
subs = [
srt.Subtitle(1, timedelta(seconds=1), timedelta(seconds=3), "Hello"),
srt.Subtitle(2, timedelta(seconds=4), timedelta(seconds=6), "World"),
]
mgr = SRTManager(subs)
Empty manager
mgr = SRTManager()
# or
mgr = SRTManager([])
Properties
mgr = SRTManager.from_file("movie.srt")
len(mgr) # number of subtitles
bool(mgr) # False if empty
repr(mgr) # "SRTManager(42 subtitles, duration=0:48:22)"
mgr.start # timedelta — start of first subtitle
mgr.end # timedelta — end of last subtitle
mgr.duration # timedelta — total span (end - start)
Iterating
for sub in mgr:
print(sub.index, sub.start, sub.content)
Index access
first = mgr[0] # srt.Subtitle object (0-based)
last = mgr[-1] # last subtitle
Note:
manager[2:5]is intentionally not supported because2:5could mean indexes or seconds — ambiguous. Usemgr.slice(2, 5)explicitly.
Membership
"hello" in mgr # True if any subtitle contains "hello" (case-insensitive)
some_subtitle in mgr # True if that exact srt.Subtitle object is present
Shifting & Timing
Shift by seconds
# Shift 3 seconds later
later = mgr >> 3
later = mgr.shift(3)
# Shift 1.5 seconds earlier
earlier = mgr << 1.5
earlier = mgr.shift(-1.5)
Subtitles that would fall below t=0 are clamped at zero. Each timestamp
(start and end) is clamped independently so duration is only affected
when the clamp is unavoidable.
# Example: subtitle at 0.5s→1.5s, shift back 1s
# start → max(0.5-1, 0) = 0.0
# end → max(1.5-1, 0) = 0.5 (duration preserved: 1s)
# Example: subtitle at 0.2s→0.8s, shift back 1s
# start → max(0.2-1, 0) = 0.0
# end → max(0.8-1, 0) = 0.0 (both clamped — subtitle becomes zero-duration)
Rescale duration
Change the total duration while keeping relative timing proportional:
# Scale everything to fit in exactly 30 minutes
mgr.duration = 1800 # seconds (int/float)
from datetime import timedelta
mgr.duration = timedelta(minutes=30) # or timedelta
The absolute start position is preserved; only the relative spacing scales.
Slicing
Extract a time window. Subtitles that partially overlap the window are clipped.
# Seconds (int or float)
clip = mgr.slice(60, 120) # 60s → 120s
# timedelta
from datetime import timedelta
clip = mgr.slice(timedelta(minutes=1), timedelta(minutes=2))
# Open-ended
clip = mgr.slice(start=60) # from 60s to end
clip = mgr.slice(end=120) # from beginning to 120s
# start=0 works correctly
clip = mgr.slice(0, 30) # from t=0 to t=30s
# Keep original timestamps (don't reset to 0)
clip = mgr.slice(60, 120, reset_time=False)
By default (reset_time=True) the result is shifted so it starts at t=0.
Merging
Combine two managers with +. If the second manager overlaps the first,
it is automatically shifted forward to eliminate the overlap.
combined = intro + main + credits
# Adding a single srt.Subtitle
import srt
from datetime import timedelta
new_sub = srt.Subtitle(0, timedelta(seconds=10), timedelta(seconds=12), "Extra line")
updated = mgr + new_sub
Searching
Find by text
# Case-insensitive (default)
results = mgr.find("hello")
results = mgr["hello"] # shorthand
# Case-sensitive
results = mgr.find("Hello", case_sensitive=True)
Returns a new SRTManager with only matching subtitles. Returns an empty
manager (falsy) if nothing matches.
results = mgr.find("xyz")
if not results:
print("Nothing found")
Check membership
if "error" in mgr:
print("Found an error subtitle")
Editing Content
Map a function over all content
# Uppercase everything
upper = mgr.map_content(str.upper)
# Strip HTML bold tags
clean = mgr.map_content(lambda t: t.replace("<b>", "").replace("</b>", ""))
Find and replace
# Case-insensitive replacement (default)
fixed = mgr.replace_content("colour", "color")
# Case-sensitive
fixed = mgr.replace_content("OK", "Okay", case_sensitive=True)
Export as plain text
text = mgr.to_plain_text() # HTML tags stripped, newline-separated
text = mgr.to_plain_text(sep=" ") # space-separated
text = mgr.to_plain_text(strip_tags=False) # keep HTML tags as-is
Collapse all subtitles into one
single = mgr.join_as_single() # returns srt.Subtitle or None if empty
single = mgr.join_as_single(sep=" | ") # custom separator
Note: The resulting subtitle spans
start → endof the entire manager, including any gaps between subtitles. Callcompress_gaps()first if you want a tight span.
Splitting
Split a manager into parts at delimiter subtitles.
# Default delimiter is "<line>"
parts = mgr.split()
# Custom delimiter
parts = mgr.split(delimiter="---")
for part in parts:
print(f"Part has {len(part)} subtitles, duration={part.duration}")
Each part starts at t=0 (time-reset automatically).
Consecutive delimiters produce empty SRTManager instances — check with bool(part).
Typical use: Mark section boundaries in your SRT with a delimiter subtitle, then split and process/save each section independently.
sections = mgr.split("<chapter>")
for i, section in enumerate(sections):
if section:
section.save(f"chapter_{i+1}.srt")
Gap Compression
Remove silences between subtitles. Each subtitle is placed immediately after the previous one, preserving individual durations.
compressed = mgr.compress_gaps()
Useful before join_as_single() to get a tight span with no embedded gaps.
single = mgr.compress_gaps().join_as_single()
Retime / Remove / Insert
Change timestamps of one subtitle
# By index (1-based, as displayed in SRT files)
updated = mgr.retime(index=5, start=10.0, end=13.5)
from datetime import timedelta
updated = mgr.retime(index=5, start=timedelta(seconds=10), end=timedelta(seconds=13.5))
Raises SRTValidationError if the new timestamps overlap adjacent subtitles.
Remove a subtitle
shorter = mgr.remove(index=3) # remove subtitle #3
Insert a subtitle
import srt
from datetime import timedelta
new_sub = srt.Subtitle(
index=0, # placeholder — will be reindexed automatically
start=timedelta(seconds=25),
end=timedelta(seconds=27),
content="New subtitle here",
)
updated = mgr.insert(new_sub)
Diffing
Compare two managers to find what changed. Identity is determined by
(start, end, content) — not by index, since indexes are reassigned
on every normalisation.
diff = original.diff(edited)
print("Added:")
for sub in diff["added"]:
print(f" [{sub.start}] {sub.content}")
print("Removed:")
for sub in diff["removed"]:
print(f" [{sub.start}] {sub.content}")
Modifications appear as one entry in
removed(old version) and one inadded(new version). There is no separate"modified"key.
Exporting & Saving
Save to file
mgr.save("output.srt")
# Non-UTF-8 encoding
mgr.save("output.srt", encoding="latin-1")
Convert to pandas DataFrame
df = mgr.to_dataframe()
# Columns: index, start (seconds), end (seconds), duration (seconds), content
# Example: find subtitles longer than 5 seconds
long_ones = df[df["duration"] > 5]
Requires pandas: pip install pandas.
Copy
copy = mgr.copy()
Mutation: add_raw
This is the only method that modifies the manager in place. All other methods return new instances.
import srt
from datetime import timedelta
new_subs = [
srt.Subtitle(0, timedelta(seconds=100), timedelta(seconds=102), "Extra"),
]
mgr.add_raw(new_subs) # mgr itself is changed
Raises SRTValidationError if any of the new subtitles overlap existing ones.
Error Reference
All errors subclass SRTValidationError:
| Condition | Message |
|---|---|
| Negative timestamp | "Subtitle N: negative timestamp detected." |
end before start |
"Subtitle N: end (...) before start (...)." |
| Overlapping subtitles | "Overlap between subtitle A (...→...) and B (...→...)." |
Wrong type to shift/slice |
TypeError: "Expected timedelta/int/float, got X." |
| Wrong type to merge | TypeError: "Cannot merge SRTManager with X." |
from srtmanager import SRTValidationError
try:
mgr.retime(5, start=50, end=40) # end before start
except SRTValidationError as e:
print(f"Timing error: {e}")
Full Workflow Examples
1. Fix subtitle delay
Video was muxed 2.3 seconds late — shift subtitles to compensate:
mgr = SRTManager.from_file("movie.srt")
fixed = mgr >> 2.3
fixed.save("movie_fixed.srt")
2. Extract a highlight clip
mgr = SRTManager.from_file("lecture.srt")
# Extract minutes 10–25
clip = mgr.slice(600, 1500)
clip.save("highlight.srt")
3. Translate: export text → reimport
mgr = SRTManager.from_file("original.srt")
# Export plain text for a translator
with open("text_for_translation.txt", "w") as f:
for sub in mgr:
f.write(f"{sub.index}|{sub.content}\n")
# After translation, reimport content
translations = {}
with open("translated.txt") as f:
for line in f:
idx, content = line.strip().split("|", 1)
translations[int(idx)] = content
translated = mgr.map_content(lambda t: t) # start from copy
import srt
new_subs = [
srt.Subtitle(sub.index, sub.start, sub.end, translations.get(sub.index, sub.content))
for sub in mgr
]
SRTManager(new_subs).save("translated.srt")
4. Merge intro + main + credits
intro = SRTManager.from_file("intro.srt")
main = SRTManager.from_file("main.srt")
credits = SRTManager.from_file("credits.srt")
full = intro + main + credits
full.save("full_movie.srt")
5. Split a long file into chapters
Mark chapter boundaries in your SRT with a special subtitle:
42
00:22:10,000 --> 00:22:10,500
<chapter>
Then:
mgr = SRTManager.from_file("documentary.srt")
chapters = mgr.split("<chapter>")
for i, chapter in enumerate(chapters, start=1):
if chapter:
chapter.save(f"chapter_{i:02d}.srt")
print(f"Chapter {i}: {len(chapter)} subs, {chapter.duration}")
6. Find & fix a mistimed subtitle
mgr = SRTManager.from_file("movie.srt")
# Find which subtitle has the wrong text
results = mgr.find("shoudl") # typo in content
print(results[0].index) # e.g. prints 73
# Fix its timing and the typo
fixed = (
mgr
.retime(73, start=445.2, end=447.8)
.replace_content("shoudl", "should")
)
fixed.save("movie_fixed.srt")
7. Analyse subtitles with pandas
mgr = SRTManager.from_file("movie.srt")
df = mgr.to_dataframe()
# Subtitles longer than 7 seconds (likely need splitting)
print(df[df["duration"] > 7][["index", "duration", "content"]])
# Average subtitle duration
print(df["duration"].mean())
# Total spoken word count
total_words = df["content"].str.split().str.len().sum()
print(f"Total words: {total_words}")
8. Scale subtitles to a re-encoded video
Video was sped up by 5% — scale all subtitle timestamps:
mgr = SRTManager.from_file("original.srt")
original_duration = mgr.duration.total_seconds()
new_duration = original_duration / 1.05 # 5% faster
mgr.duration = new_duration
mgr.save("rescaled.srt")
9. Diff two versions
v1 = SRTManager.from_file("subtitles_v1.srt")
v2 = SRTManager.from_file("subtitles_v2.srt")
diff = v1.diff(v2)
print(f"Added: {len(diff['added'])}")
print(f"Removed: {len(diff['removed'])}")
for sub in diff["added"]:
print(f" + [{sub.start}] {sub.content}")
for sub in diff["removed"]:
print(f" - [{sub.start}] {sub.content}")
Project details
Release history Release notifications | RSS feed
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 srtmanager-0.2.0.tar.gz.
File metadata
- Download URL: srtmanager-0.2.0.tar.gz
- Upload date:
- Size: 11.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.6 {"installer":{"name":"uv","version":"0.10.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72b6fa030463b550866e16fef83c5f2497ec14dd9e1eb1361ff81efb5bd54700
|
|
| MD5 |
30fb5417bf0768687b3bc9409e6c41e3
|
|
| BLAKE2b-256 |
2437b225392c0b3e6a2903473a4138a7bf720a85baaea7a68b280f017fd8a48d
|
File details
Details for the file srtmanager-0.2.0-py3-none-any.whl.
File metadata
- Download URL: srtmanager-0.2.0-py3-none-any.whl
- Upload date:
- Size: 12.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.6 {"installer":{"name":"uv","version":"0.10.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4a0d134abde79e7fa5b9636d8a72baed56fe143ec02805f315c1dc038dc9b791
|
|
| MD5 |
c89845429f49b839e481a8a367e4aaa9
|
|
| BLAKE2b-256 |
8b88dca7859a314f1599d4b1c23c26df5020247078a1a06ac1595e434181c05a
|