From 984654649c37d450b4a2fb7a0975ad6589e009a0 Mon Sep 17 00:00:00 2001 From: Franco Zanardi Date: Sat, 10 Jan 2026 11:55:01 -0300 Subject: [PATCH] fix: scaling subtitles properly across different resolutions This could introduce some visual changes --- src/pycaps/renderer/css_subtitle_renderer.py | 19 +++++++++++++++---- .../renderer/pictex_subtitle_renderer.py | 18 +++++++++++++++--- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/pycaps/renderer/css_subtitle_renderer.py b/src/pycaps/renderer/css_subtitle_renderer.py index 99c8f07..b347c8a 100644 --- a/src/pycaps/renderer/css_subtitle_renderer.py +++ b/src/pycaps/renderer/css_subtitle_renderer.py @@ -15,7 +15,10 @@ class CssSubtitleRenderer(SubtitleRenderer): - DEFAULT_DEVICE_SCALE_FACTOR: int = 2 + BASE_DEVICE_SCALE_FACTOR: float = 2.0 + REFERENCE_VIDEO_HEIGHT: int = 1280 + MIN_SCALE_MODIFIER: float = 0.25 + MAX_SCALE_MODIFIER: float = 5.0 DEFAULT_VIEWPORT_HEIGHT_RATIO: float = 0.25 DEFAULT_MIN_VIEWPORT_HEIGHT: int = 150 @@ -38,6 +41,12 @@ def __init__(self, browser: Optional['Browser'] = None): self._current_line: Optional[Line] = None self._current_line_state: Optional[ElementState] = None self._renderer_page: RendererPage = RendererPage() + self._device_scale_factor: float = self.BASE_DEVICE_SCALE_FACTOR + + def _calculate_scale_modifier(self, video_height: int) -> float: + """Calculates a scale modifier based on video height relative to reference.""" + modifier = video_height / self.REFERENCE_VIDEO_HEIGHT + return max(self.MIN_SCALE_MODIFIER, min(self.MAX_SCALE_MODIFIER, modifier)) def append_css(self, css: str): self._custom_css += css @@ -49,6 +58,8 @@ def open(self, video_width: int, video_height: int, resources_dir: Optional[Path if self._page: raise RuntimeError("Renderer is already open. Call close() first.") + scale_modifier = self._calculate_scale_modifier(video_height) + self._device_scale_factor = self.BASE_DEVICE_SCALE_FACTOR * scale_modifier calculated_vp_height = max(self.DEFAULT_MIN_VIEWPORT_HEIGHT, int(video_height * self.DEFAULT_VIEWPORT_HEIGHT_RATIO)) self._cache_strategy = cache_strategy @@ -66,7 +77,7 @@ def open(self, video_width: int, video_height: int, resources_dir: Optional[Path " playwright install chromium\n\n" f"Full error:\n{str(e)}" ) from e - context = self._browser.new_context(device_scale_factor=self.DEFAULT_DEVICE_SCALE_FACTOR, viewport={"width": video_width, "height": calculated_vp_height}) + context = self._browser.new_context(device_scale_factor=self._device_scale_factor, viewport={"width": video_width, "height": calculated_vp_height}) self._page = context.new_page() self._copy_resources_to_tempdir(resources_dir) path = self._create_html_page() @@ -223,7 +234,7 @@ def get_word_size(self, word: Word, line_state: ElementState, word_state: Elemen cached_width = sum(s.width for s in cached_letters_size.values()) cached_height = max(s.height for s in cached_letters_size.values()) if cached_letters_size else 0 if len(not_cached_letters_size) == 0: - return int(cached_width * self.DEFAULT_DEVICE_SCALE_FACTOR), int(cached_height * self.DEFAULT_DEVICE_SCALE_FACTOR) + return int(cached_width * self._device_scale_factor), int(cached_height * self._device_scale_factor) script = f""" ([letters, lineCssClasses, wordCssClasses]) => {{ @@ -256,7 +267,7 @@ def get_word_size(self, word: Word, line_state: ElementState, word_state: Elemen height = max(cached_height, max(s.height for s in new_letters_size.values())) # This is not precise, but it is enough to create the basic structure - return int(width * self.DEFAULT_DEVICE_SCALE_FACTOR), int(height * self.DEFAULT_DEVICE_SCALE_FACTOR) + return int(width * self._device_scale_factor), int(height * self._device_scale_factor) def close(self): """Closes Playwright and cleans up resources.""" diff --git a/src/pycaps/renderer/pictex_subtitle_renderer.py b/src/pycaps/renderer/pictex_subtitle_renderer.py index ec2300a..4eaeb49 100644 --- a/src/pycaps/renderer/pictex_subtitle_renderer.py +++ b/src/pycaps/renderer/pictex_subtitle_renderer.py @@ -12,7 +12,11 @@ class PictexSubtitleRenderer(SubtitleRenderer): DEFAULT_CSS_CLASS_FOR_EACH_WORD: str = "word" DEFAULT_CSS_CLASS_FOR_EACH_LINE: str = "line" - + BASE_SCALE_FACTOR: float = 2.0 + REFERENCE_VIDEO_HEIGHT: int = 1280 + MIN_SCALE_MODIFIER: float = 0.25 + MAX_SCALE_MODIFIER: float = 5.0 + def __init__(self): super().__init__() self._custom_css: str = "" @@ -22,11 +26,19 @@ def __init__(self): self._original_cwd: Optional[Path] = None self._cache_strategy = CacheStrategy.CSS_CLASSES_AWARE self._image_cache: RenderedImageCache = None + self._scale_factor: float = self.BASE_SCALE_FACTOR + + def _calculate_scale_modifier(self, video_height: int) -> float: + """Calculates a scale modifier based on video height relative to reference.""" + modifier = video_height / self.REFERENCE_VIDEO_HEIGHT + return max(self.MIN_SCALE_MODIFIER, min(self.MAX_SCALE_MODIFIER, modifier)) def append_css(self, css: str): self._custom_css += css def open(self, video_width: int, video_height: int, resources_dir: Optional[Path] = None, cache_strategy: CacheStrategy = CacheStrategy.CSS_CLASSES_AWARE): + scale_modifier = self._calculate_scale_modifier(video_height) + self._scale_factor = self.BASE_SCALE_FACTOR * scale_modifier self._resources_dir = resources_dir self._cache_strategy = cache_strategy self._image_cache = RenderedImageCache(self._custom_css, self._cache_strategy) @@ -56,7 +68,7 @@ def render_word(self, index: int, word: Word, state: ElementState, first_n_lette renderer = Html2Pic(self.get_html(line_css_classes, word_css_classes, text), self._custom_css) canvas, root_element = renderer.translator.translate(renderer.styled_tree, renderer.font_registry) try: - image = canvas.render(root_element, crop_mode=CropMode.CONTENT_BOX, scale_factor=2) + image = canvas.render(root_element, crop_mode=CropMode.CONTENT_BOX, scale_factor=self._scale_factor) pillow_image = image.to_pillow() self._image_cache.set(index, word.text, all_css_classes, first_n_letters, pillow_image) self._go_to_original_cwd() @@ -87,7 +99,7 @@ def get_word_size(self, word: Word, line_state: ElementState, word_state: Elemen renderer = Html2Pic(self.get_html(line_css_classes, word_css_classes, word.text), self._custom_css) canvas, root_element = renderer.translator.translate(renderer.styled_tree, renderer.font_registry) try: - image = canvas.render(root_element, crop_mode=CropMode.CONTENT_BOX, scale_factor=2) + image = canvas.render(root_element, crop_mode=CropMode.CONTENT_BOX, scale_factor=self._scale_factor) self._image_cache.set(-1, word.text, all_css_classes, None, image.to_pillow()) self._go_to_original_cwd() return (image.width, image.height)