Skip to content

Add Arabic subtitle support - #1210

Open
MhAhmadAli wants to merge 1 commit into
harry0703:mainfrom
MhAhmadAli:feat/arabic-subtitle-support
Open

Add Arabic subtitle support#1210
MhAhmadAli wants to merge 1 commit into
harry0703:mainfrom
MhAhmadAli:feat/arabic-subtitle-support

Conversation

@MhAhmadAli

Copy link
Copy Markdown

Closes #1205.

Adding an Arabic font alone is not enough — Arabic subtitles fail in three independent places. All three are fixed here.

Root causes

1. No bundled font has Arabic glyphs. All nine files in resource/fonts fail the project's own subtitle_font_supports_text check for Arabic, so subtitles render as tofu boxes.

2. There is no text shaping / bidi engine. This is the real blocker. Pillow wheels no longer bundle Raqm — PIL.features.check("raqm") is False on both linux/amd64 and linux/arm64 with the Pillow that moviepy==2.2.1 pulls in. Pillow then falls back to BASIC layout, so Arabic renders as isolated, unconnected letters in left-to-right order — unreadable even with a correct Arabic font.

Pillow still loads libraqm at runtime, so installing the system package is enough to flip it:

result for مرحبا بالعالم
Pillow as shipped disconnected letters, reversed order
+ libraqm0 correctly joined, right-to-left, mixed AR/Latin bidi correct

libraqm0 is available on both image bases (python:3.11-slim-bullseye and nvidia/cuda:...-ubuntu22.04).

3. The subtitle box clips fonts with large vertical metrics. MoviePy computes text height via Pillow's private _multiline_spacing, which newer Pillow removed; it then falls back to the glyph ink bbox while still positioning the baseline by ascent. Fonts whose ascent + descent exceeds their ink height get their descenders cut off. This is not Arabic-specific — the bundled CJK fonts simply have compact metrics, so it never surfaced — but it bites immediately for typical Arabic fonts.

Changes

  • resource/fonts — add Tajawal-Regular.ttf / Tajawal-Bold.ttf (SIL OFL, ~60KB each) plus the license text. Tajawal covers Arabic, Latin and both ASCII and Arabic-Indic digits, so mixed-script subtitles come from one font. Noto Sans Arabic was rejected because it contains no Latin letters — verified with this project's own glyph checker — so any subtitle mixing Arabic with an English word or brand name would show tofu. Cairo publishes only a variable font upstream. get_all_fonts() already discovers .ttf, so the picker needs no change.
  • Dockerfile, Dockerfile.gpu — install libraqm0.
  • app/services/video.py — floor the subtitle height at the font's ascent + descent + 2 * stroke_width; center the no-background subtitle by its visible pixels (the background branches already did this); detect RTL text and warn once in the task log when no shaping engine is present.
  • webui/Main.py + all 9 locales — warn in the subtitle settings panel when the script contains RTL text but Raqm is unavailable, so the user finds out before rendering rather than after.
  • READMEs (zh/en/ja) — note the libraqm requirement for source installs; the Docker images now include it.
  • Tests — cover the new font's script coverage, RTL detection, and the height floor including its fallback.

Testing

  • Full suite: 602 passed, 11 skipped. ruff check clean.
  • End-to-end through generate_video on a real render: Arabic renders joined and right-to-left; mixed Arabic/Latin bidi is correct (مرحبا MoneyPrinterTurbo 2026 places مرحبا on the right); plain, rectangular-background and rounded-background paths all verified.
  • Vertical centering measured across every bundled font: within ±0.5px, with no clipping.

Note for reviewers

The height/centering fix changes subtitle geometry slightly for existing users: with MicrosoftYaHeiBold at size 60 the box grows from 95px to 118px and bottom-anchored text moves up about 26px. It is the more correct result — centering is now exact and tall-metric fonts no longer clip — but Tajawal's metrics are compact enough that Arabic works without it. If you would rather keep existing output byte-identical, that part can be dropped on its own without affecting Arabic support.

Known limitation

split_long_token in wrap_text still splits character by character, which breaks Arabic letter joining for a single word wider than the wrap width. Rare in practice since Arabic is space-delimited; left unchanged to keep this PR focused.

@harry0703

Copy link
Copy Markdown
Owner

Thank you for the thorough investigation and for putting together such a comprehensive Arabic subtitle implementation. The Tajawal fonts, Raqm detection, Docker updates, documentation, and tests are all greatly appreciated. I also verified the font coverage and ran the full test suite successfully on the PR branch (602 passed, 11 skipped).

Before we merge this, could you please address the following items:

  1. Please update the branch against the latest main and add Subtitle Text Shaping Unavailable to webui/i18n/it.json. A simulated merge currently produces four failures in test_webui_i18n.py because the recently added Italian locale is missing this key.
  2. The local installation path still needs some cross-platform work. On an Apple Silicon Mac, Homebrew FriBiDi is present under /opt/homebrew/lib, but Pillow still reports raqm=False until that directory is included in DYLD_LIBRARY_PATH. Please update the macOS startup path or documentation accordingly. The Windows Pillow wheel also requires an external FriBiDi DLL, so the Windows one-click package needs to bundle it or the limitation and setup steps need to be documented.
  3. _warn_missing_text_shaping() is cached globally with lru_cache, so it logs only once for the entire application process. Later RTL tasks receive no task-level warning. Please scope the once-only warning to each generate_video() invocation instead.
  4. The new metric height floor changes existing subtitle geometry for all languages. In my check, the default CJK single-line container changed from about 95 px to 118 px, while Tajawal changed only from about 98 px to 99 px. Please narrow this adjustment to the cases that actually need it, separate it from this PR, or add visual regression coverage and confirm that the broader layout change is intentional.

Thank you again for the strong contribution. We look forward to reviewing the update!

@MhAhmadAli
MhAhmadAli force-pushed the feat/arabic-subtitle-support branch from 39040f4 to 5ac1619 Compare August 21, 2026 08:25
@MhAhmadAli

Copy link
Copy Markdown
Author

Thanks for the careful review — all four items are addressed and pushed.

1. Rebase + Italian locale

Rebased onto 1d1d0d2 and added Subtitle Text Shaping Unavailable to webui/i18n/it.json. All 10 locales now carry the key, and test_webui_i18n.py passes.

2. Cross-platform local install

webui.sh now adds the Homebrew library directory to DYLD_LIBRARY_PATH on Darwin when libraqm.dylib or libfribidi.dylib is present, trying brew --prefix first, then /opt/homebrew/lib, then /usr/local/lib. It is a no-op when Homebrew is absent and does not duplicate an already-present entry.

The requirement is now documented per platform in all three READMEs: Docker needs nothing, Linux needs libraqm0, macOS needs brew install libraqm (with the manual DYLD_LIBRARY_PATH export noted for anyone launching Streamlit directly), and Windows is documented as not currently able to render RTL subtitles locally because the official Pillow wheel needs an external FriBiDi DLL that the one-click package does not ship — with Docker recommended instead.

One caveat worth stating plainly: I have no macOS or Windows machine to test against, so I implemented this from your report and verified only the shell logic (syntax, path selection order, idempotency, and the no-Homebrew no-op). I would appreciate your confirmation that the Darwin branch actually flips raqm to True on your Apple Silicon setup. If bundling the FriBiDi DLL in the Windows package is something you would like included here rather than documented, let me know and I will look at it.

3. Process-global warning

Replaced the lru_cached helper with a nonlocal flag inside generate_video(). Each invocation now warns at most once, and every later RTL task gets its own warning.

4. Subtitle geometry

Removed from this PR entirely. Both the metric height floor and the no-background visible-pixel centering are gone, along with their two tests. app/services/video.py subtitle layout is now byte-identical to main, so nothing about existing output changes for any language.

Your measurements match mine, which is what makes taking the "separate it" option easy: Tajawal only moved ~98 px to ~99 px, so Arabic support genuinely does not depend on the change. The underlying issue is still real — MoviePy falls back to the glyph ink box when Pillow's removed _multiline_spacing helper is missing, while still positioning the baseline by ascent, so fonts whose ascent + descent exceeds their ink height get clipped. It just is not this PR's problem to solve. I am happy to open a follow-up with visual regression coverage if you would like it.

Verification: 601 passed, 11 skipped (4987 subtests) and ruff check clean, run against the rebased branch with Raqm available.

One separate issue you may want to know about

While running the Docker image locally I found that the RUN block installing system dependencies can succeed while installing nothing at all. The retry loop ends with || echo "Attempt $i failed, retrying..." followed by sleep 5, and sleep's exit status becomes the loop's, so apt failures are swallowed and the layer is committed with no ffmpeg, no git, and no libraqm0. You only find out at render time. In my case apt failed because plain HTTP to deb.debian.org was blocked on my network while HTTPS worked fine.

That is independent of Arabic support, but it does mean the libraqm0 line this PR adds silently does nothing wherever the apt step fails. I have a small fix (explicit https:// sources for the non-China path, plus a check at the end of the layer so a failed install fails the build). Happy to send it as its own PR if you would like — just say the word and I will open it.

@harry0703

Copy link
Copy Markdown
Owner

Thanks, the updates address the review feedback. main has moved again and the PR now conflicts in Dockerfile after the apt install fix. Please rebase once more and keep the libraqm0 addition on the current install block; we’ll run the final checks after that.

Bundle an Arabic font and enable complex text layout so right-to-left
subtitles render with joined letters and correct word order.

- Add Tajawal Regular/Bold (SIL OFL) to resource/fonts. Tajawal covers
  Arabic, Latin and both digit sets, so mixed-script subtitles render
  from a single font. The font picker already discovers .ttf files, so
  no UI change is needed.
- Install libraqm0 in both Docker images. Pillow wheels no longer bundle
  Raqm but still load it at runtime; without it Pillow falls back to
  basic layout, which leaves Arabic letters unjoined and in reversed
  order even when the font has the glyphs.
- Add the Homebrew library directory to DYLD_LIBRARY_PATH in webui.sh on
  macOS, since Pillow dlopens libraqm/libfribidi from outside the default
  search path.
- Warn in the WebUI and once per generate_video() call when subtitles
  contain right-to-left text but no shaping engine is available, since
  the failure is otherwise silent.
- Document the requirement per platform, including that the Windows
  Pillow wheel needs an external FriBiDi DLL the one-click package does
  not ship.
@MhAhmadAli
MhAhmadAli force-pushed the feat/arabic-subtitle-support branch from 5ac1619 to cf7023f Compare August 23, 2026 17:18
@MhAhmadAli

Copy link
Copy Markdown
Author

Rebased onto 05c0baf. The conflict is resolved and the branch is clean.

Dockerfile — I dropped my old edit entirely and kept your new install block as-is, adding libraqm0 to the single install_system_dependencies() call:

apt-get install -y --no-install-recommends git ffmpeg libraqm0; \

Dockerfile.gpu keeps its own one-line addition since that block is unchanged.

Worth noting that fec2721 fixes exactly the swallowed-apt-failure problem I described in my last comment — the sleep return value masking failures, and the plain-HTTP mirrors. Your version is better than the patch I was offering, so there is nothing left for me to send there. It also matters for this PR specifically: before that fix, the libraqm0 line could have silently gone missing on any network where apt failed.

One extra change while rebasing: main gained webui/i18n/fr.json, which needed the Subtitle Text Shaping Unavailable key too. Added, so all 11 locales carry it now and test_webui_i18n.py stays green.

On the earlier geometry discussion: 8cf6726 resolves it from your side, and it lands on the same conclusion I had — ascent + descent as a stable line height rather than the glyph ink box — while also handling explicit line breaks and per-line stroke padding, which my version did not. So there is no follow-up needed from me. This PR still touches no layout code.

I squashed the two commits into one while rebasing, since the second was only review fixes to the first and replaying both would have meant resolving the same conflict twice.

Verification on the new base: 714 passed, 11 skipped (6108 subtests), ruff check clean, with Raqm available. Final diff is 23 files, +224/-2, and the only removed lines are the PIL import this PR replaces and the git ffmpeg install line it extends.

The macOS caveat from my previous comment still stands — the DYLD_LIBRARY_PATH branch in webui.sh is unverified on real Apple Silicon hardware, so a confirmation from you there would be welcome before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add Arabic font

2 participants