From f2e2a706158bb1b9f03a6852be1686f33d72547c Mon Sep 17 00:00:00 2001 From: Pcecil21 Date: Tue, 5 May 2026 14:13:48 -0500 Subject: [PATCH 1/8] docs: add decisions.md (compliance note); gitignore .claude/ --- .gitignore | 1 + decisions.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 decisions.md diff --git a/.gitignore b/.gitignore index ea1791a..abee3b6 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ ENV/ # Editors / IDEs .vscode/ +.claude/ .idea/ *.swp *.swo diff --git a/decisions.md b/decisions.md new file mode 100644 index 0000000..35aca2c --- /dev/null +++ b/decisions.md @@ -0,0 +1,20 @@ +# Decisions — video-use + +## 2026-04-24 — video-use installed; client-content restriction pending John's ADV review + +**Context:** Installed browser-use/video-use at `C:\Users\pceci\Claude\Projects\video-use` with symlink registered at `%USERPROFILE%\.claude\skills\video-use`. Skill is globally available from any directory via `claude` + an inventory/edit prompt. Outputs land in `/edit/`, never in the repo. + +**Data flow:** ElevenLabs Scribe API handles transcription. Audio leaves the machine → ElevenLabs servers → word-level transcript returned. Video itself is never uploaded; only audio is sent for transcription. ffmpeg runs locally for rendering. + +**Constraint (compliance):** No client meeting recordings, prospect calls, RIA-identifiable audio, or any content containing client PII runs through this skill until John reviews the ElevenLabs data flow as part of the ADV Part 2A AI disclosure work. This is the same gating constraint that applies to other cloud AI processing of client data. + +**Permitted use today:** +- Personal video +- Youth sports (Jr. Trevians, JTL/Lax Lab, LaxVerse promo) +- Marketing/launch videos (SnapShooter, ShelfIQ, DirectorOps) +- NSIA-related non-confidential content (not board executive session recordings) + +**Open items:** +- Add ElevenLabs to the RIA vendor/data-flow inventory for John's ADV review +- Once cleared, document permitted client-content scenarios and any required disclosures +- Revisit whether self-hosted Whisper (local transcription, no cloud) is a better fit for any client-adjacent use case even post-clearance From 604f10b05175e842b8b6aeda6fae90ce9d2bcb98 Mon Sep 17 00:00:00 2001 From: Pcecil21 Date: Thu, 18 Jun 2026 20:59:44 -0500 Subject: [PATCH 2/8] chore(skill): tag video-use with risk: caution in frontmatter Marks the skill as caution-risk to reflect the compliance gate in decisions.md (ElevenLabs Scribe sends audio off-machine; no client PII until John's ADV review). Frontmatter-only; LF endings preserved. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ls7GRoTW8Nh4Bm3DKg8TWC --- SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/SKILL.md b/SKILL.md index fa4b776..349aa17 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,5 +1,6 @@ --- name: video-use +risk: caution description: Edit any video by conversation. Transcribe, cut, color grade, generate overlay animations, burn subtitles — for talking heads, montages, tutorials, travel, interviews. No presets, no menus. Ask questions, confirm the plan, execute, iterate, persist. Production-correctness rules are hard; everything else is artistic freedom. --- From d38d5bc1d994c4bd0dece749650474fa00ef5f54 Mon Sep 17 00:00:00 2001 From: Pcecil21 Date: Thu, 18 Jun 2026 21:01:43 -0500 Subject: [PATCH 3/8] chore: add .gitattributes to force LF on markdown Prevents a Windows editor from re-flipping .md files to CRLF, which produces unreviewable whole-file diffs (the SKILL.md incident). Only governs *.md; existing files are left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ls7GRoTW8Nh4Bm3DKg8TWC --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..78d838d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Force LF on markdown so a Windows editor can't re-flip these to CRLF +# and produce unreviewable whole-file diffs (see SKILL.md CRLF incident). +*.md text eol=lf From b2e8ea80acf759309040f9873d004dfcded8d6ef Mon Sep 17 00:00:00 2001 From: Pcecil21 Date: Fri, 19 Jun 2026 04:04:50 -0500 Subject: [PATCH 4/8] chore: enforce LF on .py/.sh/.toml in .gitattributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRLF in a shell script breaks execution on Linux/Codex, which the README markets support for. The existing rule only covered *.md; this extends LF enforcement to executable source. Files are already LF, so renormalize is a no-op — this is preventive hardening. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ls7GRoTW8Nh4Bm3DKg8TWC --- .gitattributes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitattributes b/.gitattributes index 78d838d..86da300 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,9 @@ # Force LF on markdown so a Windows editor can't re-flip these to CRLF # and produce unreviewable whole-file diffs (see SKILL.md CRLF incident). *.md text eol=lf + +# Executable source: CRLF in a shell script breaks execution on Linux/Codex, +# which the README markets support for. Keep these LF everywhere. +*.py text eol=lf +*.sh text eol=lf +*.toml text eol=lf From 3d22f7b40745198e05e7ec6ccee32416e565a924 Mon Sep 17 00:00:00 2001 From: Pcecil21 Date: Fri, 19 Jun 2026 06:23:59 -0500 Subject: [PATCH 5/8] docs(decisions): record John's clearance of ElevenLabs for client content Supersedes the 2026-04-24 restriction. John approved the ElevenLabs Scribe data flow for full client content, unconditional, verbal. The gate barring client-PII audio from this skill is lifted. Old entry kept intact; lift recorded on top. Open item: capture a written confirmation into the ADV file for the audit trail. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ls7GRoTW8Nh4Bm3DKg8TWC --- decisions.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/decisions.md b/decisions.md index 35aca2c..07249da 100644 --- a/decisions.md +++ b/decisions.md @@ -18,3 +18,18 @@ - Add ElevenLabs to the RIA vendor/data-flow inventory for John's ADV review - Once cleared, document permitted client-content scenarios and any required disclosures - Revisit whether self-hosted Whisper (local transcription, no cloud) is a better fit for any client-adjacent use case even post-clearance + +## 2026-06-19 — ElevenLabs data flow cleared for client content (supersedes the 2026-04-24 restriction) + +**Decision:** John approved the ElevenLabs Scribe data flow for **full client content**. The 2026-04-24 restriction — barring client meeting recordings, prospect calls, RIA-identifiable audio, and any client-PII audio from this skill — is **lifted**. All such content may now be transcribed and edited through video-use. + +**Scope:** Full. No client-content carve-outs remain. + +**Conditions:** None. Approval was unconditional. + +**Documentation basis:** Verbal approval, per Pete, 2026-06-19. No written ADV-file record captured yet (see open item). + +**Open items:** +- **Documentation hygiene (not a usage blocker):** capture a one-line written confirmation of John's approval into the ADV Part 2A AI-disclosure file / vendor inventory. Verbal-only sign-off for a third-party cloud vendor that receives client PII is thin for an exam trail — a dated email or memo closes that gap. +- The `risk: caution` tag on SKILL.md can remain (audio still leaves the machine to a third party, so "caution" stays factually accurate even when use is permitted) or be revisited — Pete's call. +- Self-hosted Whisper remains worth evaluating as a no-cloud alternative for the most sensitive material, even though cloud use is now permitted. From 7a5669737fe041f5372b3bfb39d9e96915585872 Mon Sep 17 00:00:00 2001 From: Pcecil21 Date: Fri, 19 Jun 2026 06:25:25 -0500 Subject: [PATCH 6/8] chore(skill): risk caution -> cleared after John's client-content approval ElevenLabs data flow cleared for full client content (decisions.md 2026-06-19), so the skill is no longer caution-gated. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ls7GRoTW8Nh4Bm3DKg8TWC --- SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SKILL.md b/SKILL.md index 349aa17..cd778cc 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: video-use -risk: caution +risk: cleared description: Edit any video by conversation. Transcribe, cut, color grade, generate overlay animations, burn subtitles — for talking heads, montages, tutorials, travel, interviews. No presets, no menus. Ask questions, confirm the plan, execute, iterate, persist. Production-correctness rules are hard; everything else is artistic freedom. --- From 8c4c2659508ff880d9b5efc497d58910693260dc Mon Sep 17 00:00:00 2001 From: Pcecil21 Date: Fri, 19 Jun 2026 07:45:20 -0500 Subject: [PATCH 7/8] fix(helpers): make grade/pack/render/transcribe_batch run on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three cross-platform bugs surfaced running on a Windows (cp1252) console: 1. UnicodeEncodeError on the '→'/'—' chars in status print() calls — Windows stdout defaults to cp1252 and can't encode them. grade.py crashed before grading; pack_transcripts.py crashed after writing. Fix: force UTF-8 on stdout/stderr at import in the four helpers that print non-ASCII (no-op where already UTF-8). 2. grade.py --analyze passed a Windows temp path into the ffmpeg 'metadata=print:file=...' filter, where ':' and '\' are filtergraph delimiters, so ffmpeg errored out. Fix: pass a bare filename and set ffmpeg's cwd to the file's directory (_metadata_filter_target), so no path ever reaches the filtergraph parser. Adds tests/test_grade_path.py pinning the filter-target invariant. Verified: pack_transcripts and grade --analyze both run clean on Windows with no PYTHONUTF8 workaround. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Ls7GRoTW8Nh4Bm3DKg8TWC --- helpers/grade.py | 33 ++++++++++++++++++++++++++-- helpers/pack_transcripts.py | 10 +++++++++ helpers/render.py | 10 +++++++++ helpers/transcribe_batch.py | 10 +++++++++ tests/test_grade_path.py | 43 +++++++++++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 tests/test_grade_path.py diff --git a/helpers/grade.py b/helpers/grade.py index becf6fb..2a8890b 100644 --- a/helpers/grade.py +++ b/helpers/grade.py @@ -34,6 +34,16 @@ import tempfile from pathlib import Path +# Windows consoles default to cp1252, which raises UnicodeEncodeError on the +# Unicode arrows/em-dashes in the status prints below. 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 + PRESETS: dict[str, str] = { # Subtle baseline — barely perceptible cleanup. No color shift. @@ -75,6 +85,20 @@ def get_preset(name: str) -> str: # -------- Auto grade (data-driven, per-clip) -------------------------------- +def _metadata_filter_target(path: str) -> tuple[str, str]: + r"""Return (cwd, filter_value) for writing an ffmpeg metadata file safely. + + ffmpeg filtergraphs treat ':' and '\' as delimiters, so embedding a Windows + path like C:\Users\me\tmp.txt in 'metadata=print:file=...' breaks parsing + and ffmpeg exits with an error. Instead we pass the bare filename and run + ffmpeg with cwd set to the file's directory, so no special character ever + reaches the filtergraph parser. Cross-platform: on POSIX the filename is + still bare and the parent dir is a normal cwd. + """ + p = Path(path) + return str(p.parent), p.name + + def _sample_frame_stats( video: Path, start: float, @@ -100,16 +124,21 @@ def _sample_frame_stats( with tempfile.NamedTemporaryFile(mode="w+", suffix=".txt", delete=False) as f: metadata_path = f.name + # Pass a bare filename + run ffmpeg from the file's directory so no Windows + # path (with ':' and '\') ever reaches the filtergraph parser. See + # _metadata_filter_target. + metadata_dir, metadata_name = _metadata_filter_target(metadata_path) + try: cmd = [ "ffmpeg", "-y", "-hide_banner", "-nostats", "-ss", f"{start:.3f}", "-i", str(video), "-t", f"{duration:.3f}", - "-vf", f"fps={fps:.2f},signalstats,metadata=print:file={metadata_path}", + "-vf", f"fps={fps:.2f},signalstats,metadata=print:file={metadata_name}", "-f", "null", "-", ] - subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(cmd, check=True, cwd=metadata_dir, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # Parse signalstats metadata. Signalstats reports values in the NATIVE # bit depth of the decoded frame (8-bit → 0-255, 10-bit → 0-1023). We diff --git a/helpers/pack_transcripts.py b/helpers/pack_transcripts.py index 036a127..caadb2b 100644 --- a/helpers/pack_transcripts.py +++ b/helpers/pack_transcripts.py @@ -20,6 +20,16 @@ import sys from pathlib import Path +# Windows consoles default to cp1252, which raises UnicodeEncodeError on the +# Unicode arrows/em-dashes in the status prints below. 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 + def format_time(seconds: float) -> str: """Format a time in seconds as "NNN.NN" with fixed 6-char width for alignment.""" diff --git a/helpers/render.py b/helpers/render.py index 0d02cff..145e6f8 100644 --- a/helpers/render.py +++ b/helpers/render.py @@ -28,6 +28,16 @@ import sys from pathlib import Path +# Windows consoles default to cp1252, which raises UnicodeEncodeError on the +# Unicode arrows/em-dashes in the status prints below. 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 + try: from grade import get_preset, auto_grade_for_clip # same directory except Exception: diff --git a/helpers/transcribe_batch.py b/helpers/transcribe_batch.py index 5aeb1d6..c71b02c 100644 --- a/helpers/transcribe_batch.py +++ b/helpers/transcribe_batch.py @@ -22,6 +22,16 @@ from transcribe import load_api_key, transcribe_one +# Windows consoles default to cp1252, which raises UnicodeEncodeError on the +# Unicode arrows/em-dashes in the status prints below. 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 + VIDEO_EXTS = {".mp4", ".MP4", ".mov", ".MOV", ".mkv", ".MKV", ".avi", ".AVI", ".m4v"} diff --git a/tests/test_grade_path.py b/tests/test_grade_path.py new file mode 100644 index 0000000..1f49e7b --- /dev/null +++ b/tests/test_grade_path.py @@ -0,0 +1,43 @@ +r"""Regression test for cross-platform ffmpeg metadata path handling in grade.py. + +Bug: `grade.py --analyze` crashed on Windows because the signalstats metadata +filter embedded a temp path (e.g. C:\Users\me\Temp\stats.txt), and the ':' and +'\' in that path collide with ffmpeg filtergraph delimiters, so ffmpeg exited +with an error. Fix: _metadata_filter_target() returns the file's directory (to +use as ffmpeg's cwd) and the bare filename (to put in the filter), so no path +separator or drive colon ever reaches the filtergraph parser. These tests pin +that invariant. +""" +import sys +import unittest +from pathlib import Path + +# Import the helper module directly from helpers/. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "helpers")) +import grade # noqa: E402 + + +class MetadataFilterTargetTest(unittest.TestCase): + def test_filter_value_has_no_delimiters(self): + # The bare filename must contain no ':' or path separator — those are + # exactly the chars that break an ffmpeg filtergraph. + _, name = grade._metadata_filter_target(r"C:\Users\me\AppData\Local\Temp\stats.txt") + self.assertEqual(name, "stats.txt") + self.assertNotIn(":", name) + self.assertNotIn("/", name) + self.assertNotIn("\\", name) + + def test_cwd_plus_name_roundtrips(self): + # cwd joined with the bare filename must point back at the original file. + original = str(Path("/tmp/sub/stats.txt")) + cwd, name = grade._metadata_filter_target(original) + self.assertEqual(str(Path(cwd) / name), original) + + def test_posix_filename_is_bare(self): + cwd, name = grade._metadata_filter_target("/var/folders/xy/stats.txt") + self.assertEqual(name, "stats.txt") + self.assertEqual(cwd, str(Path("/var/folders/xy"))) + + +if __name__ == "__main__": + unittest.main() From 3bc5a03c88ca6b87e0eb072b9f814dc41a4260bf Mon Sep 17 00:00:00 2001 From: Pcecil21 Date: Sat, 20 Jun 2026 07:25:52 -0500 Subject: [PATCH 8/8] fix(helpers): make timeline_view and transcribe run on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the Windows-portability sweep across the CLI helpers. Two helpers were never touched by the earlier fixes (cp1252 console + ffmpeg paths): - timeline_view.py: FONT_CANDIDATES listed only macOS/Linux font paths, so on Windows every candidate missed and load_font fell to PIL's default bitmap font (which ignores the requested size), rendering filmstrip labels nearly illegibly. Add %WINDIR%-derived Consolas + Arial candidates (drive-correct, not hardcoded C:). Also force UTF-8 on stdout/stderr so a non-ASCII output path in the status print doesn't raise UnicodeEncodeError on a cp1252 console. - transcribe.py: force UTF-8 on stdout/stderr for the same reason — it prints the video filename, which crashes the helper on Windows if non-ASCII. Adds tests/test_timeline_fonts.py (cross-platform; pins the candidate list, the %WINDIR%-derived font dir, and that load_font never raises). All 7 tests pass on Windows and Linux. No behavior change on macOS/Linux. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01NMuYbKwjCCvuaB6i75DDsF --- helpers/timeline_view.py | 21 ++++++++++++++++ helpers/transcribe.py | 11 ++++++++ tests/test_timeline_fonts.py | 49 ++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 tests/test_timeline_fonts.py diff --git a/helpers/timeline_view.py b/helpers/timeline_view.py index 0b8f72a..b52d95c 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 26d3906..28356be 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 0000000..a23a25c --- /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()