Skip to main content

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 .srt subtitle files.


Table of Contents

  1. Installation
  2. Quick Start
  3. Core Concepts
  4. Creating a Manager
  5. Properties
  6. Shifting & Timing
  7. Slicing
  8. Merging
  9. Searching
  10. Editing Content
  11. Splitting
  12. Gap Compression
  13. Retime / Remove / Insert
  14. Diffing
  15. Exporting & Saving
  16. Mutation: add_raw
  17. Error Reference
  18. 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 because 2:5 could mean indexes or seconds — ambiguous. Use mgr.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 → end of the entire manager, including any gaps between subtitles. Call compress_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 in added (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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

srtmanager-0.2.0.tar.gz (11.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

srtmanager-0.2.0-py3-none-any.whl (12.1 kB view details)

Uploaded Python 3

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

Hashes for srtmanager-0.2.0.tar.gz
Algorithm Hash digest
SHA256 72b6fa030463b550866e16fef83c5f2497ec14dd9e1eb1361ff81efb5bd54700
MD5 30fb5417bf0768687b3bc9409e6c41e3
BLAKE2b-256 2437b225392c0b3e6a2903473a4138a7bf720a85baaea7a68b280f017fd8a48d

See more details on using hashes here.

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

Hashes for srtmanager-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4a0d134abde79e7fa5b9636d8a72baed56fe143ec02805f315c1dc038dc9b791
MD5 c89845429f49b839e481a8a367e4aaa9
BLAKE2b-256 8b88dca7859a314f1599d4b1c23c26df5020247078a1a06ac1595e434181c05a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page