Skip to content

vsmuxtools.extension.sub

SubFile

SubFile(file: PathLike | Sequence[PathLike] | GlobSearch, container_delay: int = 0, source: PathLike | None = None, tags: dict[str, str] | None = None, encoding: str = 'utf_8_sig')

Bases: SubFile

Utility class representing an ASS/SSA subtitle file with various functions to run on.

Parameters:

Name Type Description Default
file PathLike | Sequence[PathLike] | GlobSearch

Can be a string, Path object or GlobSearch. If the GlobSearch returns multiple results or if a list was passed it will merge them.

required
container_delay int

Set a container delay used in the muxing process later.

0
source PathLike | None

The file this sub originates from, will be set by the constructor.

None
encoding str

Encoding used for reading and writing the subtitle files.

'utf_8_sig'
Source code in muxtools/subtitle/sub.py
def __init__(
    self,
    file: PathLike | Sequence[PathLike] | GlobSearch,
    container_delay: int = 0,
    source: PathLike | None = None,
    tags: dict[str, str] | None = None,
    encoding: str = "utf_8_sig",
):
    if isinstance(file, GlobSearch):
        file = file.paths

    if isinstance(file, list) and len(file) > 1:
        debug("Merging sub files...", self)
        docs: list[Document] = []
        for i, f in enumerate(file):
            f = ensure_path_exists(f, self)
            with open(f, "r", encoding=self.encoding) as read:
                doc = parseDoc(read)
                docs.append(doc)
                if i != 0:
                    self._warn_mismatched_properties(docs[0], doc, ensure_path(file[0], self).name, f.name)

        main = docs[0]
        existing_styles = [style.name for style in (main.styles)]
        docs.remove(main)

        for doc in docs:
            main.events.extend(doc.events)
            for style in doc.styles:
                if style.name.casefold() in [s.casefold() for s in existing_styles]:
                    warn(f"Ignoring style '{style.name}' due to preexisting style of the same name.", self)
                    continue
                main.styles.append(style)

        source = file[0]
        out = make_output(file[0], "ass", "merged")
        with open(out, "w", encoding=self.encoding) as writer:
            main.dump_file(writer)

        file = out
        debug("Done")
    else:
        file = ensure_path_exists(file, self)
        source = file
        if not os.path.samefile(file.parent, get_workdir()):
            out = make_output(file, "ass", "vof")
            with open(out, "w", encoding=self.encoding) as writer:
                self._read_doc(file).dump_file(writer)
            file = out

    super().__init__(file, container_delay, source, tags, encoding)

truncate_by_video

truncate_by_video(source: PathLike | VideoTrack | MkvTrack | VideoFile | VideoNode, timesource: TimeSourceT = None, timescale: TimeScaleT = TimeScale.MKV) -> Self

Removes lines that start after the video ends and trims lines that extend past it.

Parameters:

Name Type Description Default
source PathLike | VideoTrack | MkvTrack | VideoFile | VideoNode

Can be any video file or a VideoNode

required
timesource TimeSourceT

The source of timestamps/timecodes. For details check the docstring on the type.

None
timescale TimeScaleT

Unit of time (in seconds) in terms of which frame timestamps are represented.

For details check the docstring on the type.

MKV
Source code in vsmuxtools/extension/sub.py
def truncate_by_video(
    self,
    source: PathLike | VideoTrack | MkvTrack | VideoFile | vs.VideoNode,
    timesource: TimeSourceT = None,
    timescale: TimeScaleT = TimeScale.MKV,
) -> Self:
    """
    Removes lines that start after the video ends and trims lines that extend past it.

    :param source:          Can be any video file or a VideoNode
    :param timesource:      The source of timestamps/timecodes. For details check the docstring on the type.
    :param timescale:       Unit of time (in seconds) in terms of which frame timestamps are represented.\n
                            For details check the docstring on the type.
    """

    if isinstance(source, vs.VideoNode):
        frames = source.num_frames
        if not timesource:
            timesource = Fraction(source.fps_num, source.fps_den)
        resolved_ts = resolve_timesource_and_scale(timesource, timescale, allow_warn=False, caller=self)
    else:
        if isinstance(source, VideoTrack) or isinstance(source, MkvTrack) or isinstance(source, VideoFile):
            file = ensure_path_exists(source.file, self)
        else:
            file = ensure_path_exists(source, self)

        parsed = ParsedFile.from_file(file, self)

        assert parsed.find_tracks(type=TrackType.VIDEO, error_if_empty=True, caller=self)[0]

        meta = get_timemeta_from_video(file, caller=self)
        frames = len(meta.pts)
        if not timesource:
            timesource = meta
            timescale = meta.timescale
        resolved_ts = resolve_timesource_and_scale(timesource, timescale, allow_warn=False, caller=self)

    cutoff = timedelta(milliseconds=resolved_ts.frame_to_time(frames + 1, TimeType.START, 2, True) * 10)

    def filter_lines(lines: LINES):
        removed = 0
        trimmed = 0
        new_list = list[_Line]()
        for line in lines:
            if line.start > cutoff:
                removed += 1
                continue
            if line.end > cutoff:
                line.end = timedelta(milliseconds=resolved_ts.frame_to_time(frames, TimeType.END, 2, True) * 10)
                trimmed += 1
            new_list.append(line)

        if removed or trimmed:
            if removed:
                debug(f"Removed {removed} line{'s' if removed != 1 else ''} that started past the video", self)
            if trimmed:
                debug(f"Trimmed {trimmed} line{'s' if trimmed != 1 else ''} that extended past the video", self)

        return new_list

    return self.manipulate_lines(filter_lines)

SubFilePGS

SubFilePGS(file: PathLike | list[PathLike] | GlobSearch, container_delay: int = 0, source: PathLike | None = None, tags: dict[str, str] | None = None)

Bases: SubFilePGS

Utility class representing a PGS/SUP subtitle file.

Parameters:

Name Type Description Default
file PathLike | list[PathLike] | GlobSearch

Can be a string, Path object or GlobSearch.

required
container_delay int

Set a container delay used in the muxing process later.

0
source PathLike | None

The file this sub originates from, will be set by the constructor.

None
Source code in muxtools/subtitle/sub_pgs.py
def __init__(
    self,
    file: PathLike | list[PathLike] | GlobSearch,
    container_delay: int = 0,
    source: PathLike | None = None,
    tags: dict[str, str] | None = None,
):
    """
    :param file:            Can be a string, Path object or GlobSearch.
    :param container_delay: Set a container delay used in the muxing process later.
    :param source:          The file this sub originates from.
    :param tags:            Custom matroska tags to assign to this as a track later on.
    """
    super().__init__(file, container_delay, source, tags)
    parsed = ParsedFile.from_file(self.file, self)
    parsed_track = parsed.find_tracks(type=TrackType.SUB, relative_id=0, error_if_empty=True, caller=self)[0]
    if parsed_track.codec_name.lower() != "hdmv_pgs_subtitle":
        raise error(f"The passed file is not a PGS subtitle file. ({parsed_track.codec_name})", caller=self)

extract_from classmethod

extract_from(fileIn: PathLike | src_file, track: int = 0, preserve_delay: bool = False, quiet: bool = True) -> Self

Extract a PGS subtitle track from a file using ffmpeg.

Parameters:

Name Type Description Default
fileIn PathLike | src_file

The input file to extract from. A src_file may have at most one trim range.

required
track int

The track number to extract.

0
preserve_delay bool

If True, the container delay will be preserved.

False
quiet bool

If True, suppresses ffmpeg output.

True

Returns:

Type Description
Self

An instance of SubFilePGS containing the extracted subtitle.

Source code in vsmuxtools/extension/sub.py
@classmethod
def extract_from(cls: type[Self], fileIn: PathLike | src_file, track: int = 0, preserve_delay: bool = False, quiet: bool = True) -> Self:
    """
    Extract a PGS subtitle track from a file using ffmpeg.\n

    :param fileIn:          The input file to extract from. A src_file may have at most one trim range.
    :param track:           The track number to extract.
    :param preserve_delay:  If True, the container delay will be preserved.
    :param quiet:           If True, suppresses ffmpeg output.
    :return:                An instance of SubFilePGS containing the extracted subtitle.
    """
    f = fileIn
    trim = fileIn.trim if isinstance(fileIn, src_file) else None
    if isinstance(trim, list) and len(trim) > 1:
        raise error("Cannot currently extract PGS subtitles with multiple trim ranges.", cls.__name__)
    if isinstance(trim, list):
        trim = trim[0] if trim else None
    if trim and trim[1] == 0:
        raise error("Trim ranges cannot end with 0; use None for an open end.", cls.__name__)
    if isinstance(f, src_file):
        if isinstance(f.file, list):
            # I'll make a workaround for this soonish
            raise error("Cannot currently extract PGS subtitles when splicing multiple files.", cls.__name__)
        f = f.file
    new = super().extract_from(
        f,
        track=track,
        preserve_delay=preserve_delay,
        quiet=quiet,
    )
    if isinstance(fileIn, src_file) and trim:
        start, end = trim
        if start:
            debug(f"Shifting extracted subtitle by {-start} frames...", cls.__name__)
            new = new.shift(-start, timesource=fileIn.file, quiet=quiet)
        if end:
            warn("Trimming is currently not supported for PGS subtitles.", cls.__name__)
    return new