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()
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]
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
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
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]
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
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)))
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)))
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]
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]
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()
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)
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]
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}")
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]
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()
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]
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)
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
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
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
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 ]
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