Subtitle burn-in is broken two ways: crashes on a documented macOS install, and renders tofu for non-Latin scripts
Two bugs in the same code path, both reproducible from a clean install following install.md. Happy to send a PR for either.
1. brew install ffmpeg gives you an ffmpeg without libass, so --build-subtitles crashes
install.md says brew install ffmpeg. On current Homebrew (ffmpeg 8.1.2, arm64 macOS) the default bottle is not built with --enable-libass, so the subtitles, ass, and drawtext filters do not exist at all:
$ ffmpeg -hide_banner -version | grep -o enable-libass
(no output)
$ ffmpeg -hide_banner -filters | grep -E ' (subtitles|ass|drawtext) '
(no output)Any run with --build-subtitles (or an EDL with a subtitles field) dies with an unhandled CalledProcessError:
subprocess.CalledProcessError: Command '['ffmpeg', '-y', '-i', '.../base.mp4',
'-filter_complex', "[0:v]subtitles='.../master.srt':force_style='FontName=Helvetica,...'[outv]",
...]' returned non-zero exit status 234.The underlying ffmpeg error is a filter-graph parse failure, which is confusing because it looks like a quoting problem rather than a missing filter:
[AVFilterGraph] No option name near '/.../master.srt:force_style=FontName=Helvetica,FontSize=18'
[AVFilterGraph] Error parsing a filter description around: [outv]Note that master.srt is generated correctly before this — only the burn-in step fails, so everything up to the last stage looks like it worked.
Suggested fix: preflight the filter before rendering, and fail with an actionable message instead of a traceback. Something like:
def has_subtitles_filter() -> bool:
out = subprocess.run(["ffmpeg", "-hide_banner", "-filters"],
capture_output=True, text=True).stdout
return bool(re.search(r"^\s*\S+\s+subtitles\s", out, re.M))and in install.md, document that the default Homebrew bottle lacks libass. Working options on macOS are brew install ffmpeg-full (bottled, has libass — but it is keg-only, so /opt/homebrew/opt/ffmpeg-full/bin has to come first on PATH for helpers to pick it up), or building ffmpeg from source against libass.
2. SUB_FORCE_STYLE hardcodes FontName=Helvetica, so any non-Latin script renders as tofu
render.py:51:
SUB_FORCE_STYLE = (
"FontName=Helvetica,FontSize=18,Bold=1,"
...
)Because this goes through force_style, it overrides whatever the subtitle file specifies, and libass does no glyph fallback here. With a Chinese transcript the cues render as empty boxes.
This is worth fixing because it is an internal inconsistency rather than a niche gap: Scribe transcribes 90+ languages, and the README lists travel and interview footage as target use cases, but the subtitle layer only renders Latin script. Anyone editing CJK, Thai, Arabic, Hebrew, or Devanagari footage gets a clean transcript and unreadable subtitles.
Suggested fix: let the EDL override the style, e.g. an optional subtitle_style or subtitle_font key merged over the default, so the skill can pick a font per project without patching the module constant. A sensible default on macOS would be a font with wide coverage that is present out of the box.
One gotcha worth documenting if you do add font selection: on macOS, PingFang SC is not resolvable through fontconfig (fc-match "PingFang SC" silently returns Verdana), so it still renders tofu. Heiti SC resolves to /System/Library/Fonts/STHeiti Medium.ttc and works. Arial Unicode MS also has broad coverage.
For reference, this is the only change I needed — same geometry, font swapped:
force_style='FontName=Heiti SC,FontSize=18,Bold=1,PrimaryColour=&H00FFFFFF,
OutlineColour=&H00000000,BorderStyle=1,Outline=2,Shadow=0,Alignment=2,MarginV=90'FontSize=18 and MarginV=90 are correct as-is, by the way — libass resolves them against its default script resolution, which lands the baseline right about where the comment above the constant says it should.
Minor, related: the caption chunker assumes word-delimited Latin text
build_master_srt groups into 2-word chunks and calls .upper() (render.py:370). For CJK, .upper() is a no-op, and 2-token chunks cut across utterance boundaries, so unrelated phrases get merged into one cue. On a 31s clip with 47 word tokens I got 24 cues, several of which joined two separate utterances.
Grouping on inter-word gaps instead (plus a per-cue character cap) gave 14 cues that matched what was actually said. Probably worth gating the chunking rule on language_code rather than changing the English default.
Environment
- macOS arm64 (Darwin 27.0), Homebrew ffmpeg 8.1.2 (default bottle), Python 3.12
- Source: 720x1280 H.265 portrait clip, Chinese speech
Source: browser-use/video-use