fix(helpers): UTF-8 and correct ffmpeg escaping for non-ASCII paths on Windows - #116
fix(helpers): UTF-8 and correct ffmpeg escaping for non-ASCII paths on Windows#116gocc0 wants to merge 1 commit into
Conversation
…s on Windows
Rendering failed outright whenever the footage directory contained non-ASCII
characters (e.g. "Gravações de Tela"), and auto-grade failed on Windows even
with pure-ASCII paths. Four distinct defects, one root cause: the helpers
assumed the locale codepage and assumed ffmpeg parses paths like the OS does.
1. concat_segments() wrote _concat.txt with write_text() and no encoding. That
uses the locale codepage (cp1252 on pt-BR Windows) while ffmpeg's concat
demuxer reads the list as UTF-8, so "Gravações" landed in the file as the
bytes Grava\347\365es, every entry resolved to a missing file, and ffmpeg
exited -22 / EINVAL. Now written as UTF-8, with entries relative to edit_dir
and ffmpeg run with cwd=edit_dir so non-ASCII stays out of the list entirely.
2. build_final_composite() escaped ':' and "'" in the subtitles= path but left
backslashes intact. Backslash is the filtergraph escape character, so
C:\Users\me\master.srt reached the filter as C:Usersmemaster.srt ("Unable to
open"). New escape_filter_path() normalizes to forward slashes and escapes
the drive colon. Accents/CJK/Cyrillic then work with no copy needed; a
literal "'" is the one character the parser cannot carry in any quoting form,
so filtergraph_safe_path() stages a temp copy for that case only.
3. grade.py passed a raw Windows path to metadata=print:file= inside a
filtergraph, which fails to parse on every Windows machine regardless of
accents ("Error parsing a filter description") and took --grade auto down.
Same quoting fix applied.
4. Progress lines carrying '→' — or an accented source filename — raised
UnicodeEncodeError on a cp1252 console and killed the run before any ffmpeg
work started. use_utf8_stdio() reconfigures stdout/stderr to UTF-8.
Also adds explicit encoding="utf-8" to every read_text/write_text/open in
helpers/ that can touch a user path or transcript text.
Verified on Windows 11 / ffmpeg 8.1.2 with an end-to-end render from an
accented directory, an accented source filename, accented caption text and
auto-grade: passes after, fails before at concat (-22), subtitles (-2) and
auto-grade. ASCII paths unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
4 issues found across 6 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/grade.py">
<violation number="1" location="helpers/grade.py:109">
P2: Auto-grade still fails when the temp-directory path contains an apostrophe (for example, a Windows profile named `O'Connor`): this closes `file=` quoting before FFmpeg parses the filter. Use the same staged safe-path approach as `filtergraph_safe_path()` for the metadata output path, rather than embedding this pathname directly.</violation>
</file>
<file name="helpers/render.py">
<violation number="1" location="helpers/render.py:65">
P3: `use_utf8_stdio()` is now copy-pasted verbatim into five files (render.py, grade.py, transcribe.py, timeline_view.py, pack_transcripts.py), while transcribe_batch.py correctly imports it from transcribe (`from transcribe import ... use_utf8_stdio`). This 12-line helper is identical everywhere and fixing one copy (e.g. choosing `errors="replace"` vs `strict`) won't propagate to the others, which is exactly how encoding bugs spread. Consider centralizing it in a shared module (e.g. transcribe.py already exports it, or a small `_utf8.py` util) and importing it in the other four scripts, mirroring transcribe_batch.py.</violation>
<violation number="2" location="helpers/render.py:146">
P2: Subtitles still fail for users whose Windows temp path contains an apostrophe: staging inherits that apostrophe and the helper only warns before invoking ffmpeg. Select or create a quote-free staging root, or fail before launching with a clear actionable error.</violation>
<violation number="3" location="helpers/render.py:358">
P2: Renders with an apostrophe in an EDL source key still fail during concat because the generated `file '…'` record has an unescaped quote. Escape single quotes using concat-demuxer quoting when emitting each entry.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| # ("Error parsing a filter description"), which took auto-grade down on | ||
| # every Windows machine regardless of accents. Quote it, use forward | ||
| # slashes, and escape the drive colon. | ||
| meta_arg = "'" + metadata_path.replace("\\", "/").replace(":", r"\:") + "'" |
There was a problem hiding this comment.
P2: Auto-grade still fails when the temp-directory path contains an apostrophe (for example, a Windows profile named O'Connor): this closes file= quoting before FFmpeg parses the filter. Use the same staged safe-path approach as filtergraph_safe_path() for the metadata output path, rather than embedding this pathname directly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/grade.py, line 109:
<comment>Auto-grade still fails when the temp-directory path contains an apostrophe (for example, a Windows profile named `O'Connor`): this closes `file=` quoting before FFmpeg parses the filter. Use the same staged safe-path approach as `filtergraph_safe_path()` for the metadata output path, rather than embedding this pathname directly.</comment>
<file context>
@@ -101,12 +101,18 @@ def _sample_frame_stats(
+ # ("Error parsing a filter description"), which took auto-grade down on
+ # every Windows machine regardless of accents. Quote it, use forward
+ # slashes, and escape the drive colon.
+ meta_arg = "'" + metadata_path.replace("\\", "/").replace(":", r"\:") + "'"
cmd = [
"ffmpeg", "-y", "-hide_banner", "-nostats",
</file context>
| yield path | ||
| return | ||
|
|
||
| tmp_dir = Path(tempfile.mkdtemp(prefix="video_use_")) |
There was a problem hiding this comment.
P2: Subtitles still fail for users whose Windows temp path contains an apostrophe: staging inherits that apostrophe and the helper only warns before invoking ffmpeg. Select or create a quote-free staging root, or fail before launching with a clear actionable error.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 146:
<comment>Subtitles still fail for users whose Windows temp path contains an apostrophe: staging inherits that apostrophe and the helper only warns before invoking ffmpeg. Select or create a quote-free staging root, or fail before launching with a clear actionable error.</comment>
<file context>
@@ -92,6 +108,55 @@ def resolve_path(maybe_path: str, base: Path) -> Path:
+ yield path
+ return
+
+ tmp_dir = Path(tempfile.mkdtemp(prefix="video_use_"))
+ try:
+ staged = tmp_dir / path.name.replace("'", "_")
</file context>
| else: | ||
| entries.append(rel.as_posix()) | ||
| concat_list.write_text( | ||
| "".join(f"file '{e}'\n" for e in entries), encoding="utf-8" |
There was a problem hiding this comment.
P2: Renders with an apostrophe in an EDL source key still fail during concat because the generated file '…' record has an unescaped quote. Escape single quotes using concat-demuxer quoting when emitting each entry.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 358:
<comment>Renders with an apostrophe in an EDL source key still fail during concat because the generated `file '…'` record has an unescaped quote. Escape single quotes using concat-demuxer quoting when emitting each entry.</comment>
<file context>
@@ -265,21 +330,47 @@ def extract_all_segments(
+ else:
+ entries.append(rel.as_posix())
+ concat_list.write_text(
+ "".join(f"file '{e}'\n" for e in entries), encoding="utf-8"
+ )
</file context>
| "".join(f"file '{e}'\n" for e in entries), encoding="utf-8" | |
| "".join(f"file '{e.replace(\"'\", r\"'\\''\")}'\n" for e in entries), encoding="utf-8" |
| # -------- Helpers ------------------------------------------------------------ | ||
|
|
||
|
|
||
| def use_utf8_stdio() -> None: |
There was a problem hiding this comment.
P3: use_utf8_stdio() is now copy-pasted verbatim into five files (render.py, grade.py, transcribe.py, timeline_view.py, pack_transcripts.py), while transcribe_batch.py correctly imports it from transcribe (from transcribe import ... use_utf8_stdio). This 12-line helper is identical everywhere and fixing one copy (e.g. choosing errors="replace" vs strict) won't propagate to the others, which is exactly how encoding bugs spread. Consider centralizing it in a shared module (e.g. transcribe.py already exports it, or a small _utf8.py util) and importing it in the other four scripts, mirroring transcribe_batch.py.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 65:
<comment>`use_utf8_stdio()` is now copy-pasted verbatim into five files (render.py, grade.py, transcribe.py, timeline_view.py, pack_transcripts.py), while transcribe_batch.py correctly imports it from transcribe (`from transcribe import ... use_utf8_stdio`). This 12-line helper is identical everywhere and fixing one copy (e.g. choosing `errors="replace"` vs `strict`) won't propagate to the others, which is exactly how encoding bugs spread. Consider centralizing it in a shared module (e.g. transcribe.py already exports it, or a small `_utf8.py` util) and importing it in the other four scripts, mirroring transcribe_batch.py.</comment>
<file context>
@@ -58,6 +62,18 @@ def auto_grade_for_clip(video, start=0.0, duration=None, verbose=False): # type
# -------- Helpers ------------------------------------------------------------
+def use_utf8_stdio() -> None:
+ """Print through UTF-8 rather than the locale codepage.
+
</file context>
Rendering fails outright on a non-English Windows whenever the footage
directory contains non-ASCII characters, and
--grade autofails on Windowseven with pure-ASCII paths. Four distinct defects, one root cause: the helpers
assume the locale codepage is UTF-8, and assume ffmpeg parses a path the same
way the OS does.
Repro for the original symptom — footage in
C:\Users\me\Videos\Gravações de Tela:1.
concat_segments()wrote the concat list in the locale codepageconcat_list.write_text(...)with noencodinguses the locale codepage(cp1252 on pt-BR Windows), while ffmpeg's concat demuxer reads the list as
UTF-8.
Gravaçõeswas written as the bytesGrava\347\365es, so every entryresolved to a nonexistent file and ffmpeg exited
-22/EINVAL.Fixed by writing UTF-8, and additionally by writing entries relative to
edit_dirand running ffmpeg withcwd=edit_dir, which keeps non-ASCII out ofthe list file altogether. Segments outside
edit_dirfall back to an absoluteentry, which is safe now that the file is UTF-8.
2.
subtitles=was passed an unescaped Windows pathbuild_final_composite()escaped:and'but left backslashes intact.Backslash is the filtergraph escape character, so
C:\Users\me\master.srtreached the filter as
C:Usersmemaster.srt:Worth noting for anyone who has worked around this: accents were never the
problem here. Verified on ffmpeg 8.1.2 that Latin-1, CJK and Cyrillic
subtitle paths all load correctly once the path uses forward slashes and an
escaped drive colon — no ASCII copy required. The new
escape_filter_path()does exactly that.
The one character ffmpeg's two-level parser genuinely cannot carry in a
filename is a literal
'— every quoting and escaping form of it fails toinitialize the filter.
filtergraph_safe_path()stages a temp copy for thatcase only.
3.
grade.pypassed a raw Windows path tometadata=print:file=Same class, different filter, and this one is not accent-related at all — it
breaks
--grade autoon every Windows machine:Same quoting fix applied.
4.
UnicodeEncodeErrorwhen printing progressWindows selects the console codepage for
sys.stdout, so a progress linecontaining
→— or an accented source filename — raisesUnicodeEncodeErrorand kills the run before any ffmpeg work starts.
use_utf8_stdio()reconfigures stdout/stderr to UTF-8; it is a no-op on Linux and macOS.
Also
Explicit
encoding="utf-8"on everyread_text/write_text/openinhelpers/that can touch a user path or transcript text, including the masterSRT write (libass reads SRT as UTF-8, and captions carry non-ASCII text even
when the paths are ASCII).
Testing
Windows 11, ffmpeg 8.1.2, Python 3.14, pt-BR locale (
cp1252).End-to-end
render.py --build-subtitlesfrom a directory namedGravações de Tela, with an accented source filename, accented caption text,and
"grade": "auto":OLÁ MUNDOrenders with the accent intact
-22), at concat (-22), and at subtitles(
-2)ASCII-path renders were checked for regression and are unaffected. Also
verified a directory containing both an apostrophe and accents
(
Gui's Gravações) now renders, via the staged-copy path.Summary by cubic
Fixes Windows rendering failures with non-ASCII paths by using UTF-8 consistently and proper
ffmpegfiltergraph path escaping. Restores subtitles and--grade auto, and prevents console encoding crashes._concat.txtas UTF-8, use edit-relative entries, and runffmpegwithcwd=edit_dir.escape_filter_path(); stage a temp copy if the path contains'withfiltergraph_safe_path().metadata=print:file=path inside the filtergraph.use_utf8_stdio(); read/write JSON, SRT, and.envwithencoding="utf-8".Written for commit 03dea21. Summary will update on new commits.