fix: Windows compatibility for subtitle rendering, path escaping and console encoding - #143
fix: Windows compatibility for subtitle rendering, path escaping and console encoding#143emiloza123 wants to merge 3 commits into
Conversation
- render.py: normalize the .srt path to forward slashes before escaping the colon for ffmpeg's subtitles filter. Escaping only ':' left raw Windows backslashes in the filtergraph string, which ffmpeg's parser can't handle (crashed with exit 4294967294 when burning subtitles). - render.py: write master.srt with explicit encoding="utf-8". Path.write_text() otherwise falls back to the OS locale encoding (cp1252 on Windows), which mangles Spanish accents/ñ. - render.py, pack_transcripts.py: force stdout to UTF-8 on import via sys.stdout.reconfigure(). Both scripts print arrows (→) that raise UnicodeEncodeError under the legacy Windows console codepage. - render.py: default subtitle font Helvetica -> Arial. Helvetica isn't installed on Windows and relied on fontconfig guessing a substitute; Arial is a native system font on both Windows and macOS. All four fixes normalize to platform-agnostic behavior (forward-slash paths, explicit UTF-8) rather than special-casing Windows, so macOS/Linux are unaffected.
Apply the same sys.stdout.reconfigure(encoding="utf-8") guard used in render.py and pack_transcripts.py to grade.py, timeline_view.py, and transcribe_batch.py, so their own arrow (→) prints don't raise UnicodeEncodeError under the legacy Windows console codepage either. Repo-wide grep confirms no other .py file prints non-ASCII characters without this guard now in place. Also noted (not fixed, out of scope here): grade.py's _sample_frame_stats() passes an unescaped Windows temp path with a drive-letter colon into an ffmpeg metadata= filter option, which fails the same way the subtitles path bug did before c0c2c27 — a separate pre-existing issue, hit only via `grade.py --analyze`.
_sample_frame_stats() passed the raw tempfile path into an ffmpeg metadata=print:file=... filter option. On Windows that path contains a drive-letter colon and backslashes, both of which are filtergraph syntax characters, so ffmpeg failed to parse it (same failure mode as the subtitles path bug fixed in c0c2c27). Apply the same treatment as that fix: normalize backslashes to forward slashes, then escape the colon and any literal quotes, before embedding the path in the filter string. The plain filesystem path (used with open() to read the stats back) is left untouched. POSIX temp paths have no backslashes, so the platform-agnostic fix is a no-op there. Verified: `grade.py --analyze videotry1.mp4` runs clean end to end with no environment variable workarounds.
There was a problem hiding this comment.
3 issues found across 5 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="helpers/render.py">
<violation number="1" location="helpers/render.py:68">
P3: `SUB_FORCE_STYLE` now emits Arial, but the documented `bold-overlay` contract still says Helvetica and identifies `render.py` as its implementation. Update the style documentation to Arial so users do not get an undocumented font change.</violation>
<violation number="2" location="helpers/render.py:671">
P2: When the subtitle path contains an apostrophe, the generated filtergraph does not escape it using FFmpeg's required close-escape-reopen form, so subtitle rendering can fail. Encode apostrophes as `r"'\\''"` before the existing outer quotes.</violation>
</file>
<file name="helpers/grade.py">
<violation number="1" location="helpers/grade.py:124">
P2: When the Windows temp path contains an apostrophe, `_sample_frame_stats` emits an invalid FFmpeg filtergraph and auto-grading fails. Encode apostrophes as `r"'\\''"` before wrapping the path in single quotes.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| # backslashes, so the replace is a no-op there. `metadata_path` itself | ||
| # (used below with plain `open()`) stays untouched. | ||
| metadata_path_filter = metadata_path.replace("\\", "/") | ||
| metadata_path_filter = metadata_path_filter.replace(":", r"\:").replace("'", r"\'") |
There was a problem hiding this comment.
P2: When the Windows temp path contains an apostrophe, _sample_frame_stats emits an invalid FFmpeg filtergraph and auto-grading fails. Encode apostrophes as r"'\\''" before wrapping the path in single quotes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/grade.py, line 124:
<comment>When the Windows temp path contains an apostrophe, `_sample_frame_stats` emits an invalid FFmpeg filtergraph and auto-grading fails. Encode apostrophes as `r"'\\''"` before wrapping the path in single quotes.</comment>
<file context>
@@ -100,13 +110,26 @@ def _sample_frame_stats(
+ # backslashes, so the replace is a no-op there. `metadata_path` itself
+ # (used below with plain `open()`) stays untouched.
+ metadata_path_filter = metadata_path.replace("\\", "/")
+ metadata_path_filter = metadata_path_filter.replace(":", r"\:").replace("'", r"\'")
+
try:
</file context>
| # literal quotes. POSIX paths have no backslashes, so the replace is | ||
| # a no-op there and this stays identical to the prior behavior. | ||
| subs_abs = str(subtitles_path.resolve()).replace("\\", "/") | ||
| subs_abs = subs_abs.replace(":", r"\:").replace("'", r"\'") |
There was a problem hiding this comment.
P2: When the subtitle path contains an apostrophe, the generated filtergraph does not escape it using FFmpeg's required close-escape-reopen form, so subtitle rendering can fail. Encode apostrophes as r"'\\''" before the existing outer quotes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 671:
<comment>When the subtitle path contains an apostrophe, the generated filtergraph does not escape it using FFmpeg's required close-escape-reopen form, so subtitle rendering can fail. Encode apostrophes as `r"'\\''"` before the existing outer quotes.</comment>
<file context>
@@ -641,7 +659,16 @@ def build_final_composite(
+ # literal quotes. POSIX paths have no backslashes, so the replace is
+ # a no-op there and this stays identical to the prior behavior.
+ subs_abs = str(subtitles_path.resolve()).replace("\\", "/")
+ subs_abs = subs_abs.replace(":", r"\:").replace("'", r"\'")
filter_parts.append(
f"{current}subtitles='{subs_abs}':force_style='{SUB_FORCE_STYLE}'[outv]"
</file context>
| # core system font on Windows and macOS, so naming it directly removes | ||
| # that guesswork on both. Linux distros without Arial still get a | ||
| # graceful fontconfig substitution, same as before. | ||
| "FontName=Arial,FontSize=18,Bold=1," |
There was a problem hiding this comment.
P3: SUB_FORCE_STYLE now emits Arial, but the documented bold-overlay contract still says Helvetica and identifies render.py as its implementation. Update the style documentation to Arial so users do not get an undocumented font change.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 68:
<comment>`SUB_FORCE_STYLE` now emits Arial, but the documented `bold-overlay` contract still says Helvetica and identifies `render.py` as its implementation. Update the style documentation to Arial so users do not get an undocumented font change.</comment>
<file context>
@@ -50,7 +60,12 @@ def auto_grade_for_clip(video, start=0.0, duration=None, verbose=False): # type
+ # core system font on Windows and macOS, so naming it directly removes
+ # that guesswork on both. Linux distros without Arial still get a
+ # graceful fontconfig substitution, same as before.
+ "FontName=Arial,FontSize=18,Bold=1,"
"PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,BackColour=&H00000000,"
"BorderStyle=1,Outline=2,Shadow=0,"
</file context>
Summary
Five Windows-only bugs in the render pipeline, found while running video-use
end-to-end on Windows 11. All fixes normalize to platform-agnostic behavior
(forward-slash paths, explicit UTF-8) rather than special-casing Windows, so
they are no-ops on macOS/Linux. Tested on Windows 11 only.
Bugs fixed
1. Subtitle
.srtpath breaks ffmpeg's filtergraph parser (render.py,build_final_composite)The subtitles filter path was built by escaping only
:, leaving rawWindows backslashes in place. ffmpeg's filtergraph parser treats both
\and
:as syntax characters, so a path likeC:\Users\...\master.srtbroke parsing (
ffmpegexited with status 4294967294) whenever--build-subtitlesor an EDL-referenced subtitle file was used.Fix: normalize backslashes to forward slashes first (ffmpeg accepts
forward slashes on every platform, including Windows), then escape the
drive-letter colon and any literal quotes. No-op on POSIX, where paths
never contain backslashes.
2.
master.srtwritten without explicit encoding (render.py,build_master_srt)Path.write_text()falls back tolocale.getpreferredencoding()when noencoding is given — cp1252 on Windows by default, which cannot represent
Spanish accents/ñ and silently corrupts them in the burned-in captions.
Fix:
write_text(..., encoding="utf-8")explicit. Identical output onplatforms whose locale already defaults to UTF-8.
3. Console prints crash on non-ASCII characters (
pack_transcripts.py,render.py,grade.py,timeline_view.py,transcribe_batch.py)Several progress/status prints use an arrow (
→). On Windows,sys.stdout'sdefault encoding is the legacy console codepage (cp1252), which can't
encode it —
UnicodeEncodeError, crashing the script after useful workwas already done (e.g.
pack_transcripts.pycrashed only in its finalsummary print, after
takes_packed.mdhad already been written). Fix:sys.stdout.reconfigure(encoding="utf-8")guarded withhasattr/try,added once at import time in each of the five files. No message text was
changed — the info stays intact, only the stream encoding is forced.
No-op where stdout is already UTF-8.
4. Subtitle font relies on fontconfig substitution (
render.py,SUB_FORCE_STYLE)FontName=Helveticaisn't installed on Windows, so libass/fontconfig hadto guess a substitute at render time (landed on Arial-BoldMT, but with no
guarantee). Fix: name
Arialdirectly — a native system font on bothWindows and macOS. Linux distributions without Arial still fall back to
fontconfig substitution exactly as before, so there's no regression there.
5. Second instance of the path-escaping bug (
grade.py,_sample_frame_stats)Same root cause as #1, different call site: the temp file path used for
ffmpeg's
signalstats/metadata=print:file=...filter was passed raw,so
grade.py --analyzefailed on Windows the same way subtitlecompositing did. Fix: same treatment — normalize slashes, escape
:and
', in a separate variable so the plain filesystem path (usedafterward with
open()) is untouched.Known issue (not fixed in this PR)
timeline_view.pydraws an arrow (→) directly onto the generated PNGfilmstrip via PIL's
draw.text(). On Windows this renders as a broken/missing glyph box rather than crashing — it's a font-rendering gap in
whatever default font PIL falls back to, not a console-encoding issue,
and out of scope for this PR.
Test plan
transcribe.py→pack_transcripts.py→render.py --build-subtitles) on a real Spanish-language clip withno environment variable workarounds (no
PYTHONIOENCODING, noPYTHONUTF8) — completed end to end, produced a burned-in-subtitleMP4 with correctly rendered accents.
master.srtis UTF-8 viafile master.srt.(output_time = word.start - segment_start + segment_offset) — exact
match on multiple cues.
grade.py --analyzeon the same clip with no environmentvariable workarounds — completed end to end.
print(...)calls —confirms no other file has the same unguarded pattern.
Summary by cubic
Makes the render pipeline work on Windows 11 by fixing five Windows-only bugs in subtitle rendering, path escaping, and console encoding, without changing behavior on macOS or Linux.
Bug Fixes
render.py,signalstatsmetadata ingrade.py) by normalizing backslashes to forward slashes before escaping colons and quotes.master.srtwith explicitencoding="utf-8"so Spanish accents and ñ survive under Windows's cp1252 default locale.sys.stdoutto UTF-8 on import inrender.py,pack_transcripts.py,grade.py,timeline_view.py, andtranscribe_batch.py, preventingUnicodeEncodeErrorcrashes on arrow (→) prints.timeline_view.pystill shows a broken glyph for the arrow, a font-rendering issue outside this PR's scope.Written for commit 4459f63. Summary will update on new commits.