diff --git a/helpers/timeline_view.py b/helpers/timeline_view.py index 0b8f72a7..b52d95c6 100644 --- a/helpers/timeline_view.py +++ b/helpers/timeline_view.py @@ -22,6 +22,7 @@ import argparse import json +import os import subprocess import sys import tempfile @@ -31,6 +32,17 @@ from PIL import Image, ImageDraw, ImageFont +# Windows consoles default to cp1252, which raises UnicodeEncodeError when the +# status prints below include a non-ASCII path (accents, emoji, CJK). Force +# UTF-8 on stdout/stderr so this helper behaves identically on Windows, macOS, +# and Linux. No-op where the stream is already UTF-8 or can't be reconfigured. +for _stream in (sys.stdout, sys.stderr): + try: + _stream.reconfigure(encoding="utf-8") + except (AttributeError, ValueError): + pass + + # -------- Frame extraction --------------------------------------------------- @@ -151,12 +163,21 @@ def find_silences(words: list[dict], start: float, end: float, threshold: float # -------- Font loading ------------------------------------------------------- +# Windows has no fixed font directory on C:, so derive it from %WINDIR% +# (falls back to the default install path). Consolas mirrors the monospace +# intent of Menlo/DejaVuSansMono; Arial is the sans fallback. Without these, +# every candidate below misses on Windows and load_font drops to PIL's tiny +# default bitmap font, rendering the filmstrip labels nearly illegibly. +_WIN_FONTS = Path(os.environ.get("WINDIR", r"C:\Windows")) / "Fonts" + FONT_CANDIDATES = [ "/System/Library/Fonts/Menlo.ttc", "/System/Library/Fonts/Helvetica.ttc", "/System/Library/Fonts/SFNSMono.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf", + str(_WIN_FONTS / "consola.ttf"), # Consolas — Windows monospace + str(_WIN_FONTS / "arial.ttf"), # Arial — Windows sans fallback ] diff --git a/helpers/transcribe.py b/helpers/transcribe.py index 26d3906e..28356be9 100644 --- a/helpers/transcribe.py +++ b/helpers/transcribe.py @@ -27,6 +27,17 @@ import requests +# Windows consoles default to cp1252, which raises UnicodeEncodeError when the +# status prints below include a non-ASCII video filename (accents, emoji, CJK). +# Force UTF-8 on stdout/stderr so this helper behaves identically on Windows, +# macOS, and Linux. No-op where the stream is already UTF-8 or can't reconfigure. +for _stream in (sys.stdout, sys.stderr): + try: + _stream.reconfigure(encoding="utf-8") + except (AttributeError, ValueError): + pass + + SCRIBE_URL = "https://api.elevenlabs.io/v1/speech-to-text" diff --git a/tests/test_timeline_fonts.py b/tests/test_timeline_fonts.py new file mode 100644 index 00000000..a23a25c4 --- /dev/null +++ b/tests/test_timeline_fonts.py @@ -0,0 +1,49 @@ +r"""Regression test for cross-platform font selection in timeline_view.py. + +Bug: FONT_CANDIDATES listed only macOS and Linux font paths. On Windows every +candidate failed Path.exists(), so load_font fell through to PIL's default +bitmap font (which ignores the requested size), rendering the filmstrip labels +nearly illegibly. Fix: add Windows candidates derived from %WINDIR%\Fonts +(Consolas + Arial). These tests pin the candidate list and the env-derived +font dir, and confirm load_font always returns a usable font — on any host. +""" +import os +import sys +import unittest +from pathlib import Path + +HELPERS_DIR = Path(__file__).resolve().parent.parent / "helpers" +sys.path.insert(0, str(HELPERS_DIR)) +import timeline_view # noqa: E402 + + +class FontCandidatesTest(unittest.TestCase): + def test_windows_fonts_present(self): + # Both Windows fallbacks must be in the candidate list, under a Fonts dir. + names = [Path(c).name.lower() for c in timeline_view.FONT_CANDIDATES] + self.assertIn("consola.ttf", names) + self.assertIn("arial.ttf", names) + for c in timeline_view.FONT_CANDIDATES: + if Path(c).name.lower() in ("consola.ttf", "arial.ttf"): + self.assertEqual(Path(c).parent.name, "Fonts") + + def test_macos_and_linux_fonts_still_present(self): + # The fix must not drop the original POSIX candidates. + joined = " ".join(timeline_view.FONT_CANDIDATES) + self.assertIn("/System/Library/Fonts/Menlo.ttc", joined) + self.assertIn("DejaVuSansMono.ttf", joined) + + def test_win_fonts_dir_respects_windir(self): + # The Windows font dir is derived from %WINDIR%, not hardcoded to C:. + windir = os.environ.get("WINDIR", r"C:\Windows") + self.assertEqual(timeline_view._WIN_FONTS, Path(windir) / "Fonts") + + def test_load_font_never_raises(self): + # load_font must return a usable font object on any host, falling back + # to PIL's default only when no candidate file exists. + font = timeline_view.load_font(14) + self.assertIsNotNone(font) + + +if __name__ == "__main__": + unittest.main()