示例#1
0
    async def run(self) -> None:
        subs = await self.args.target.get_subtitles()
        if not subs:
            raise CommandUnavailable("nothing to update")

        start = await self.args.start.get(
            align_to_near_frame=not self.args.no_align)
        end = await self.args.end.get(
            align_to_near_frame=not self.args.no_align)

        # use beginning of dialogues as reference points for both sides of the
        # range, since it makes more sense to align by dialogue start rather
        # than by dialogue end
        old_start = subs[0].start
        old_end = subs[-1].start

        self.api.log.info(str(old_start))
        self.api.log.info(str(old_end))

        def adjust(pts: int) -> int:
            pts = int(start + (pts - old_start) * (end - start) /
                      (old_end - old_start))
            if not self.args.no_align and self.api.video.current_stream:
                pts = self.api.video.current_stream.align_pts_to_near_frame(
                    pts)
            return pts

        with self.api.undo.capture():
            for sub in subs:
                sub.begin_update()
                sub.start = adjust(sub.start)
                sub.end = adjust(sub.end)
                sub.end_update()
示例#2
0
    async def run(self) -> None:
        subs = await self.args.target.get_subtitles()
        if not subs:
            raise CommandUnavailable("nothing to merge")

        with self.api.undo.capture():
            subs[0].begin_update()

            if self.args.invisible:
                text = ""
                for i, sub in enumerate(subs):
                    text += sub.text
                    if i != len(subs) - 1:
                        pos = subs[i + 1].start - subs[0].start
                        text += r"{\alpha&HFF&\t(%d,%d,\alpha&H00&)}" % (
                            pos,
                            pos,
                        )
                subs[0].text = text
            else:
                subs[0].text = "".join(
                    ("{\\k%.01f}" % (sub.duration / 10)) + sub.text
                    for sub in subs)

            subs[0].note = "".join(sub.note for sub in subs)
            subs[0].end = subs[-1].end
            subs[0].end_update()

            assert subs[0].index is not None
            self.api.subs.events.remove(subs[0].index + 1, len(subs) - 1)
            self.api.subs.selected_indexes = [subs[0].index]
示例#3
0
 def _move_below(self, indexes: list[int]) -> Iterable[AssEvent]:
     if indexes[-1] + 1 == len(self.api.subs.events):
         raise CommandUnavailable("cannot move further down")
     for idx, count in make_ranges(indexes, reverse=True):
         chunk = [copy(s) for s in self.api.subs.events[idx:idx + count]]
         self.api.subs.events[idx + count + 1:idx + count + 1] = chunk
         del self.api.subs.events[idx:idx + count]
         yield from chunk
示例#4
0
 def _move_above(self, indexes: list[int]) -> Iterable[AssEvent]:
     if indexes[0] == 0:
         raise CommandUnavailable("cannot move further up")
     for idx, count in make_ranges(indexes):
         chunk = [copy(s) for s in self.api.subs.events[idx:idx + count]]
         self.api.subs.events[idx - 1:idx - 1] = chunk
         del self.api.subs.events[idx + count:idx + count + count]
         yield from chunk
示例#5
0
    async def run(self) -> None:
        with self.api.undo.capture():
            indexes = await self.args.target.get_indexes()
            if not indexes:
                raise CommandUnavailable("nothing to delete")

            for start_idx, count in make_ranges(indexes, reverse=True):
                del self.api.subs.events[start_idx:start_idx + count]
示例#6
0
 def _move_above(self, indexes: T.List[int]) -> T.Iterable[AssEvent]:
     if indexes[0] == 0:
         raise CommandUnavailable("cannot move further up")
     for idx, count in make_ranges(indexes):
         chunk = [copy(s) for s in self.api.subs.events[idx:idx + count]]
         self.api.subs.events.insert(idx - 1, *chunk)
         self.api.subs.events.remove(idx + count, count)
         yield from chunk
示例#7
0
    def _paste_from_clipboard(self, idx: int) -> None:
        text = QApplication.clipboard().text()
        if not text:
            raise CommandUnavailable("clipboard is empty, aborting")

        subs = AssEventList.from_ass_string(text)
        with self.api.undo.capture():
            self.api.subs.events[idx:idx] = [copy(sub) for sub in subs]
            self.api.subs.selected_indexes = list(range(idx, idx + len(subs)))
示例#8
0
    def _paste_from_clipboard(self, idx: int) -> None:
        text = QtWidgets.QApplication.clipboard().text()
        if not text:
            raise CommandUnavailable("clipboard is empty, aborting")

        items = T.cast(T.List[AssEvent], _unpickle(text))
        with self.api.undo.capture():
            self.api.subs.events.insert(idx, *items)
            self.api.subs.selected_indexes = list(range(idx, idx + len(items)))
示例#9
0
    async def run(self) -> None:
        subs = await self.args.target.get_subtitles()
        if not subs:
            raise CommandUnavailable("nothing to merge")

        if len(subs) == 1:
            if not subs[0].next:
                raise CommandUnavailable("nothing to merge")
            subs.append(subs[0].next)

        with self.api.undo.capture():
            subs[0].begin_update()
            subs[0].end = subs[-1].end
            if self.args.concat:
                subs[0].text = "".join(sub.text for sub in subs)
                subs[0].note = "".join(sub.note for sub in subs)
            subs[0].end_update()

            self.api.subs.events.remove(subs[0].index + 1, len(subs) - 1)
            self.api.subs.selected_indexes = [subs[0].index]
示例#10
0
    async def run(self) -> None:
        with self.api.undo.capture(), self.api.gui.throttle_updates():
            indexes = await self.args.target.get_indexes()
            if not indexes:
                raise CommandUnavailable("nothing to clone")

            sub_copies: list[AssEvent] = []
            for idx in reversed(indexes):
                sub_copy = copy(self.api.subs.events[idx])
                self.api.subs.events.insert(idx + 1, sub_copy)
                sub_copies.append(sub_copy)
            self.api.subs.selected_indexes = [sub.index for sub in sub_copies]
示例#11
0
    async def run(self) -> None:
        subs = await self.args.target.get_subtitles()
        if not subs:
            raise CommandUnavailable("nothing to update")

        with self.api.undo.capture():
            for sub in subs:
                params = {
                    "text": sub.text,
                    "note": sub.note,
                    "actor": sub.actor,
                    "style": sub.style_name,
                }

                sub.begin_update()

                if self.args.start:
                    sub.start = await self.args.start.get(
                        origin=sub.start,
                        align_to_near_frame=not self.args.no_align,
                    )

                if self.args.end:
                    sub.end = await self.args.end.get(
                        origin=sub.end,
                        align_to_near_frame=not self.args.no_align,
                    )

                if self.args.text is not None:
                    sub.text = self.args.text.format(**params)

                if self.args.note is not None:
                    sub.note = self.args.note.format(**params)

                if self.args.actor is not None:
                    sub.actor = self.args.actor.format(**params)

                if self.args.style is not None:
                    sub.style_name = self.args.style.format(**params)

                if self.args.comment:
                    sub.is_comment = True

                if self.args.no_comment:
                    sub.is_comment = False

                if self.args.layer is not None:
                    sub.layer = self.args.layer

                sub.end_update()
示例#12
0
    async def run(self) -> None:
        with self.api.undo.capture():
            indexes = await self.args.target.get_indexes()
            if not indexes:
                raise CommandUnavailable("nothing to delete")

            new_selection = set(self.api.subs.selected_events) - set(
                self.api.subs.events[idx] for idx in indexes)

            self.api.subs.selected_indexes = [
                sub.index for sub in new_selection if sub.index is not None
            ]
            for start_idx, count in make_ranges(indexes, reverse=True):
                self.api.subs.events.remove(start_idx, count)
示例#13
0
    async def run(self) -> None:
        subs = await self.args.target.get_subtitles()
        if not subs:
            raise CommandUnavailable("nothing to merge")

        if len(subs) == 1:
            if not subs[0].next:
                raise CommandUnavailable("nothing to merge")
            subs.append(subs[0].next)

        with self.api.undo.capture():
            subs[0].begin_update()
            subs[0].end = subs[-1].end
            if self.args.concat:
                subs[0].text = merge_text((sub.text for sub in subs),
                                          separator=self.args.separator)
                subs[0].note = merge_text((sub.note for sub in subs),
                                          separator=self.args.separator)
            subs[0].end_update()

            idx = subs[0].index
            del self.api.subs.events[idx + 1:idx + len(subs)]
            self.api.subs.selected_indexes = [idx]
示例#14
0
    async def run(self) -> None:
        start = await self.args.start.get()
        end = await self.args.end.get()
        if end < start:
            end, start = start, end
        if start == end:
            raise CommandUnavailable("nothing to sample")

        assert self.api.video.current_stream.path
        path = await self.args.path.get_save_path(
            file_filter="Webm Video File (*.webm)",
            default_file_name="video-{}-{}..{}.webm".format(
                self.api.video.current_stream.path.name,
                ms_to_str(start),
                ms_to_str(end),
            ),
        )

        def create_sample() -> None:
            with tempfile.TemporaryDirectory() as dir_name:
                subs_path = Path(dir_name) / "tmp.ass"
                with open(subs_path, "w") as handle:
                    write_ass(self.api.subs.ass_file, handle)

                command = [
                    "ffmpeg",
                    "-i",
                    str(self.api.video.current_stream.path),
                    "-y",
                    "-crf",
                    str(self.args.crf),
                    "-b:v",
                    "0",
                    "-ss",
                    ms_to_str(start),
                    "-to",
                    ms_to_str(end),
                ]

                if self.args.include_subs:
                    command += ["-vf", "ass=" + str(subs_path)]

                command += [str(path)]

                subprocess.run(command)

        # don't clog the UI thread
        self.api.log.info(f"saving video sample to {path}...")
        await asyncio.get_event_loop().run_in_executor(None, create_sample)
        self.api.log.info(f"saved video sample to {path}")
示例#15
0
    async def run(self) -> None:
        with self.api.undo.capture():
            indexes = await self.args.target.get_indexes()
            if not indexes:
                raise CommandUnavailable("nothing to move")

            if self.args.method == "above":
                sub_copies = list(self._move_above(indexes))
            elif self.args.method == "below":
                sub_copies = list(self._move_below(indexes))
            elif self.args.method == "gui":
                base_idx = await self.api.gui.exec(self._show_dialog, indexes)
                sub_copies = list(self._move_to(indexes, base_idx))
            else:
                raise AssertionError

            self.api.subs.selected_indexes = [sub.index for sub in sub_copies]
示例#16
0
    async def _run_with_gui(self, main_window: QtWidgets.QMainWindow) -> None:
        subs = await self.args.target.get_subtitles()
        if not subs:
            raise CommandUnavailable("nothing to update")

        delta = await self._get_delta(subs, main_window)

        with self.api.undo.capture():
            for sub in subs:
                sub.begin_update()
                sub.start = await delta.get(
                    origin=sub.start,
                    align_to_near_frame=not self.args.no_align,
                )
                sub.end = await delta.get(
                    origin=sub.end, align_to_near_frame=not self.args.no_align)
                sub.end_update()
示例#17
0
    async def run(self) -> None:
        subs = await self.args.target.get_subtitles()
        if not subs:
            raise CommandUnavailable("nothing to split")

        split_pos = await self.args.position.get(
            align_to_near_frame=not self.args.no_align)

        with self.api.undo.capture(), self.api.gui.throttle_updates():
            for sub in reversed(subs):
                if split_pos < sub.start or split_pos > sub.end:
                    continue
                idx = sub.index
                self.api.subs.events.insert(idx + 1, copy(sub))
                self.api.subs.events[idx].end = split_pos
                self.api.subs.events[idx + 1].start = split_pos
                self.api.subs.selected_indexes = [idx, idx + 1]
示例#18
0
    async def run(self) -> None:
        events = await self.args.target.get_subtitles()

        if len(events) != 1:
            raise CommandUnavailable("too many subtitles selected")

        with self.api.undo.capture():
            event = events[0]

            if not self.args.only_print:
                event.is_comment = True

            for i in range(self.args.steps):
                step = i / (self.args.steps - 1)
                next_step = (i + 1) / (self.args.steps - 1)

                t = get_transform(
                    self.api,
                    self.args.y1,
                    self.args.y2,
                    self.args.c1,
                    self.args.c2,
                    step,
                    next_step,
                )

                prefix = (
                    f"{{"
                    f"\\clip({t.x1:.02f},{t.y1:.02f},{t.x2:.02f},{t.y2:.02f})"
                    f"\\1c&H{t.c.b:02X}{t.c.g:02X}{t.c.r:02X}&"
                    f"}}"
                )

                new_event = copy(event)
                new_event.text = prefix + ASS_COLOR_REGEX.sub(
                    "", new_event.text
                )
                new_event.is_comment = False

                if self.args.only_print:
                    self.api.log.info(prefix)
                else:
                    self.api.subs.events.insert(event.index + 1 + i, new_event)
示例#19
0
    async def run(self) -> None:
        text = QApplication.clipboard().text()
        if not text:
            self.api.log.error("clipboard is empty, aborting")
            return

        lines = text.rstrip("\n").split("\n")
        subs = await self.args.target.get_subtitles()
        if not subs:
            raise CommandUnavailable("nothing to paste into")

        if len(lines) != len(subs):
            raise CommandError(f"size mismatch ("
                               f"selected {len(subs)} lines, "
                               f"got {len(lines)} lines in clipboard)".format(
                                   len(subs), len(lines)))

        with self.api.undo.capture():
            if self.args.subject == "text":
                for line, sub in zip(lines, subs):
                    sub.text = line

            elif self.args.subject == "notes":
                for line, sub in zip(lines, subs):
                    sub.note = line

            elif self.args.subject == "times":
                times: list[tuple[int, int]] = []
                for line in lines:
                    try:
                        start, end = line.split("-", 1)
                        times.append(
                            (str_to_ms(start.strip()), str_to_ms(end.strip())))
                    except ValueError as ex:
                        raise ValueError(
                            f"invalid time format: {line}") from ex

                for time, sub in zip(times, subs):
                    sub.start = time[0]
                    sub.end = time[1]

            else:
                raise AssertionError
示例#20
0
    async def run(self) -> None:
        subs = await self.args.target.get_subtitles()
        if not subs:
            raise CommandUnavailable("nothing to copy")

        if self.args.subject == "text":
            QtWidgets.QApplication.clipboard().setText("\n".join(
                sub.text for sub in subs))
        elif self.args.subject == "notes":
            QtWidgets.QApplication.clipboard().setText("\n".join(
                sub.note for sub in subs))
        elif self.args.subject == "times":
            QtWidgets.QApplication.clipboard().setText("\n".join(
                "{} - {}".format(ms_to_str(sub.start), ms_to_str(sub.end))
                for sub in subs))
        elif self.args.subject == "all":
            QtWidgets.QApplication.clipboard().setText(_pickle(subs))
        else:
            raise AssertionError
示例#21
0
    async def get_load_path(
        self,
        file_filter: Optional[str] = None,
    ) -> Path:
        if self.value:
            path = Path(self.value).expanduser()
            if not path.exists():
                raise CommandUnavailable(f'file "{path}" does not exist')
            return path

        path = await self.api.gui.exec(
            self._show_load_dialog,
            file_filter=file_filter,
            directory=self.api.gui.get_dialog_dir(),
        )
        if path:
            self.api.gui.last_directory = path.parent
            return path

        raise CommandCanceled
示例#22
0
    async def run(self) -> None:
        subs = await self.args.target.get_subtitles()
        if not subs:
            raise CommandUnavailable("nothing to split")

        new_selection: T.List[AssEvent] = []
        with self.api.undo.capture(), self.api.gui.throttle_updates():
            for sub in subs:
                if "\\k" not in sub.text:
                    continue

                start = sub.start
                end = sub.end
                try:
                    syllables = list(self._get_syllables(sub.text))
                except ass_tag_parser.ParseError as ex:
                    raise CommandError(str(ex))

                idx = sub.index
                self.api.subs.events.remove(idx, 1)

                new_subs: T.List[AssEvent] = []
                for i, syllable in enumerate(syllables):
                    sub_copy = copy(sub)
                    sub_copy.start = start
                    sub_copy.end = min(end, start + syllable.duration)
                    sub_copy.text = syllable.text
                    if i > 0:
                        sub_copy.note = ""
                    start = sub_copy.end
                    new_subs.append(sub_copy)

                self.api.subs.events.insert(idx, *new_subs)
                new_selection += new_subs

            self.api.subs.selected_indexes = [
                sub.index for sub in new_selection if sub.index is not None
            ]
示例#23
0
from PyQt5 import QtCore, QtGui, QtWidgets

from bubblesub.api import Api
from bubblesub.api.cmd import BaseCommand, CommandUnavailable
from bubblesub.cfg.menu import MenuCommand
from bubblesub.cmd.common import SubtitlesSelection
from bubblesub.fmt.ass.event import AssEvent
from bubblesub.ui.util import Dialog, async_dialog_exec
from bubblesub.util import ms_to_str

try:
    import cv2
    import numpy as np
    import pytesseract
except ImportError as ex:
    raise CommandUnavailable(f"{ex.name} is not installed")


class OcrSettings(QtCore.QObject):
    changed = QtCore.pyqtSignal()

    def __init__(self, parent: QtCore.QObject) -> None:
        super().__init__(parent)
        self.threshold = 128
        self.invert = False
        self.x1 = 0
        self.y1 = 0
        self.x2 = 0
        self.y2 = 0
        self.dilate = False
        self.erode = False