Skip to content

Commit 5ac1619

Browse files
committed
Address review feedback on Arabic subtitle support
- Add the shaping warning key to the new Italian locale so every WebUI language carries it after rebasing onto main. - Scope the "no shaping engine" warning to each generate_video() call instead of caching it for the whole process, so every task that renders right-to-left subtitles records the warning. - Drop the subtitle height floor and the no-background centering change. They alter subtitle geometry for every language, and the bundled Arabic font does not need them, so they belong in their own change with visual regression coverage. Subtitle layout is now identical to main. - Add the Homebrew library directory to DYLD_LIBRARY_PATH in webui.sh on macOS. Pillow dlopens libraqm/libfribidi, which Homebrew installs outside the default search path, so raqm stayed disabled without it. - Document the shaping requirement per platform, including that the Windows Pillow wheel needs an external FriBiDi DLL that the one-click package does not ship.
1 parent 0e7bc9b commit 5ac1619

7 files changed

Lines changed: 53 additions & 122 deletions

File tree

README-en.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,11 @@ Use the local setup or Docker instructions below.
231231

232232
- Local deployment requires Python 3.11 or later
233233
- On Windows, avoid project paths containing non-ASCII characters, special characters, or spaces
234-
- Right-to-left subtitles such as Arabic need libraqm installed on the system (Debian/Ubuntu: `apt install libraqm0`, macOS: `brew install libraqm`). Without it letters are not joined and word order is reversed; the Docker images already include it
234+
- Right-to-left subtitles such as Arabic need Pillow's Raqm layout engine. Without it letters are not joined and word order is reversed:
235+
- Docker: already included in the images, nothing to do
236+
- Linux: `apt install libraqm0`
237+
- macOS: `brew install libraqm`. Homebrew's libraries are not on the default search path, so `webui.sh` adds `DYLD_LIBRARY_PATH` automatically; if you launch Streamlit yourself, export `DYLD_LIBRARY_PATH="$(brew --prefix)/lib:$DYLD_LIBRARY_PATH"` first
238+
- Windows: the official Pillow wheel needs an external FriBiDi DLL on PATH, which the one-click package does not ship. Until it does, a local Windows install cannot render right-to-left subtitles correctly — use Docker instead
235239

236240
#### ① Clone the Project
237241

README-ja.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,11 @@ GitHub Releases から最新の Windows 用ワンクリックパッケージを
230230

231231
- ローカル環境へのデプロイには Python 3.11 以降が必要です
232232
- Windows では、プロジェクトのパスに非 ASCII 文字、特殊文字、スペースを含めないでください
233-
- アラビア語など右から左に書く字幕には、システムへの libraqm のインストールが必要です(Debian/Ubuntu: `apt install libraqm0`、macOS: `brew install libraqm`)。無い場合は文字が連結されず語順も逆になります。Docker イメージには同梱済みです
233+
- アラビア語など右から左に書く字幕には、Pillow の Raqm 組版エンジンが必要です。無い場合は文字が連結されず語順も逆になります:
234+
- Docker: イメージに同梱済みで、追加作業は不要です
235+
- Linux: `apt install libraqm0`
236+
- macOS: `brew install libraqm`。Homebrew のライブラリは既定の検索パスに無いため、`webui.sh``DYLD_LIBRARY_PATH` を自動で追加します。Streamlit を手動で起動する場合は `export DYLD_LIBRARY_PATH="$(brew --prefix)/lib:$DYLD_LIBRARY_PATH"` を先に実行してください
237+
- Windows: 公式の Pillow wheel は PATH 上に別途 FriBiDi DLL を必要としますが、ワンクリックパッケージには同梱されていません。同梱されるまで Windows のローカル環境では右から左の字幕を正しく描画できないため、Docker の利用を推奨します
234238

235239
#### ① プロジェクトをクローンする
236240

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,11 @@
237237

238238
- 本地部署需要 Python 3.11 或更高版本
239239
- Windows 用户建议避免使用包含中文、特殊字符或空格的项目路径
240-
- 阿拉伯语等从右到左的字幕需要系统安装 libraqm(Debian/Ubuntu:`apt install libraqm0`,macOS:`brew install libraqm`)。缺少它时字母不会连写且顺序会颠倒,Docker 镜像已内置
240+
- 阿拉伯语等从右到左的字幕需要 Pillow 启用 Raqm 排版引擎,缺少时字母不会连写、词序也会颠倒:
241+
- Docker:镜像已内置,无需额外操作
242+
- Linux:`apt install libraqm0`
243+
- macOS:`brew install libraqm`。Homebrew 的动态库不在默认搜索路径里,`webui.sh` 会自动补上 `DYLD_LIBRARY_PATH`;如果你手动启动 Streamlit,需要自行 `export DYLD_LIBRARY_PATH="$(brew --prefix)/lib:$DYLD_LIBRARY_PATH"`
244+
- Windows:官方 Pillow wheel 需要额外的 FriBiDi DLL 位于 PATH 中,一键包并未内置。在补齐之前,Windows 本地部署无法正确渲染从右到左的字幕,建议改用 Docker
241245

242246
#### ① 克隆代码
243247

app/services/video.py

Lines changed: 20 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -992,45 +992,6 @@ def subtitle_text_needs_shaping(text: str) -> bool:
992992
)
993993

994994

995-
def _subtitle_text_block_height(
996-
font_path: str,
997-
font_size: int,
998-
stroke_width: int,
999-
line_count: int,
1000-
ink_height: int,
1001-
) -> int:
1002-
"""
1003-
计算多行字幕文字实际需要的像素高度。
1004-
1005-
MoviePy 2.2.1 依赖 Pillow 的私有方法 `_multiline_spacing` 计算文本高度,
1006-
该方法在新版 Pillow 中已被移除,于是 MoviePy 退回使用字形墨迹包围盒
1007-
(`bottom - top`)。墨迹高度会明显小于字体自身的 ascent + descent,
1008-
而 MoviePy 仍按 ascent 定位基线,导致 ascent/descent 较大的字体(阿拉伯语
1009-
字体尤为典型,也包括已内置的 Charm、微软雅黑)下方被裁切。
1010-
1011-
这里改用 MoviePy 文档中声明的公式 `ascent + descent + 2 * stroke_width`
1012-
作为单行高度下限,无论 MoviePy 走哪个分支都能留出完整的绘制空间;对
1013-
ascent/descent 较小的中文字体则维持原有的墨迹高度,不改变既有观感。
1014-
"""
1015-
try:
1016-
ascent, descent = ImageFont.truetype(font_path, font_size).getmetrics()
1017-
metric_height = (ascent + descent) * max(1, line_count) + 2 * stroke_width
1018-
except Exception as e:
1019-
logger.warning(f"failed to read subtitle font metrics: {font_path}, {e}")
1020-
metric_height = 0
1021-
return int(max(ink_height, metric_height))
1022-
1023-
1024-
@lru_cache(maxsize=1)
1025-
def _warn_missing_text_shaping() -> None:
1026-
"""整段任务只提示一次,避免逐条字幕刷屏。"""
1027-
logger.warning(
1028-
"subtitle contains right-to-left text but Pillow has no Raqm layout engine; "
1029-
"letters will not be joined and word order will be reversed. "
1030-
"install libraqm (Debian/Ubuntu: libraqm0, macOS: brew install libraqm)"
1031-
)
1032-
1033-
1034995
def generate_video(
1035996
video_path: str,
1036997
audio_path: str,
@@ -1070,6 +1031,22 @@ def generate_video(
10701031

10711032
logger.info(f" ⑤ font: {font_path}")
10721033

1034+
# 缺少 Raqm 是环境级问题,但提示必须按任务给出:整个进程只提示一次会让
1035+
# 后续任务的日志完全看不到这条关键信息。这里用单次任务内的标志位收敛,
1036+
# 既不会逐条字幕刷屏,也保证每次合成都留下记录。
1037+
missing_shaping_warned = False
1038+
1039+
def warn_missing_text_shaping_once():
1040+
nonlocal missing_shaping_warned
1041+
if missing_shaping_warned:
1042+
return
1043+
missing_shaping_warned = True
1044+
logger.warning(
1045+
"subtitle contains right-to-left text but Pillow has no Raqm layout "
1046+
"engine; letters will not be joined and word order will be reversed. "
1047+
"install libraqm (Debian/Ubuntu: libraqm0, macOS: brew install libraqm)"
1048+
)
1049+
10731050
def resolve_subtitle_background_color():
10741051
# 兼容历史参数:API 里 `text_background_color` 既可能是布尔值,
10751052
# 也可能是实际颜色字符串。统一在这里归一化,避免把 True/False
@@ -1083,7 +1060,7 @@ def create_text_clip(subtitle_item):
10831060
params.stroke_width = int(params.stroke_width)
10841061
phrase = subtitle_item[1]
10851062
if subtitle_text_needs_shaping(phrase) and not text_layout_supports_shaping():
1086-
_warn_missing_text_shaping()
1063+
warn_missing_text_shaping_once()
10871064
max_width = video_width * 0.9
10881065
bg_color = resolve_subtitle_background_color()
10891066
rounded_bg_enabled = bool(
@@ -1115,14 +1092,7 @@ def create_text_clip(subtitle_item):
11151092
# 描边或背景色时,容易把最后一行的下半部分裁掉。这里显式传入
11161093
# 一个更保守的高度,把行间距和额外上下留白一并算进去,保证字幕
11171094
# 背景框与文字本身都能完整渲染出来。
1118-
text_block_height = _subtitle_text_block_height(
1119-
font_path=font_path,
1120-
font_size=params.font_size,
1121-
stroke_width=params.stroke_width,
1122-
line_count=line_count,
1123-
ink_height=int(txt_height),
1124-
)
1125-
clip_h = int(text_block_height + vertical_padding + (interline * line_count))
1095+
clip_h = int(txt_height + vertical_padding + (interline * line_count))
11261096

11271097
if rounded_bg_enabled:
11281098
# 圆角背景需要贴合文字宽度,而不是沿用 90% 视频宽度。这里先用
@@ -1203,7 +1173,7 @@ def create_text_clip(subtitle_item):
12031173
int(max_width),
12041174
clip_h,
12051175
)
1206-
text_clip = TextClip(
1176+
_clip = TextClip(
12071177
text=wrapped_txt,
12081178
font=font_path,
12091179
font_size=params.font_size,
@@ -1212,27 +1182,8 @@ def create_text_clip(subtitle_item):
12121182
stroke_color=params.stroke_color,
12131183
stroke_width=params.stroke_width,
12141184
interline=interline,
1215-
size=(int(max_width), None),
1216-
text_align="center",
1217-
margin=(0, text_clip_margin_y),
1218-
)
1219-
size = (size[0], max(size[1], text_clip.h))
1220-
# 无背景字幕同样需要按可见像素居中。MoviePy 先按墨迹高度居中、
1221-
# 再按 ascent 落基线,字形墨迹没有顶到 ascender 时文字就会偏离
1222-
# 画布中心;带背景的分支早已用透明底板 + 可见居中修正,这里沿用
1223-
# 同一套做法,只是底板完全透明,避免阿拉伯语等大 ascent/descent
1224-
# 字体的字幕整体偏上。
1225-
transparent_canvas = _rounded_subtitle_background_clip(
1226-
width=size[0],
1227-
height=size[1],
1228-
color="#000000",
1229-
alpha=0,
1230-
radius=0,
1231-
)
1232-
text_position = _get_visible_center_position(text_clip, size[0], size[1])
1233-
_clip = CompositeVideoClip(
1234-
[transparent_canvas, text_clip.with_position(text_position)],
12351185
size=size,
1186+
text_align="center",
12361187
)
12371188
duration = subtitle_item[0][1] - subtitle_item[0][0]
12381189
_clip = _clip.with_start(subtitle_item[0][0])

test/services/test_subtitle_background_settings.py

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import unittest
44

55
import numpy as np
6-
from PIL import ImageFont
76

87
from app.models.schema import SubtitleRequest, VideoParams
98
from app.services import video
@@ -196,52 +195,3 @@ def test_detects_text_that_requires_bidi_shaping(self):
196195
self.assertFalse(video.subtitle_text_needs_shaping("人工智能改变生活"))
197196
self.assertFalse(video.subtitle_text_needs_shaping("Artificial intelligence"))
198197
self.assertFalse(video.subtitle_text_needs_shaping(""))
199-
200-
def test_text_block_height_reserves_room_for_font_metrics(self):
201-
"""
202-
MoviePy 依赖的 Pillow 私有方法已被移除,导致文本高度退化成墨迹高度,
203-
ascent/descent 较大的字体下方会被裁切。字幕高度必须以字体自身的
204-
ascent + descent 为下限,同时保留墨迹更高时的原有取值。
205-
"""
206-
fonts_dir = Path(__file__).parent.parent.parent / "resource" / "fonts"
207-
arabic_font = str(fonts_dir / "Tajawal-Regular.ttf")
208-
ascent, descent = ImageFont.truetype(arabic_font, 60).getmetrics()
209-
210-
single_line = video._subtitle_text_block_height(
211-
font_path=arabic_font,
212-
font_size=60,
213-
stroke_width=1,
214-
line_count=1,
215-
ink_height=1,
216-
)
217-
two_lines = video._subtitle_text_block_height(
218-
font_path=arabic_font,
219-
font_size=60,
220-
stroke_width=1,
221-
line_count=2,
222-
ink_height=1,
223-
)
224-
ink_taller_than_metrics = video._subtitle_text_block_height(
225-
font_path=arabic_font,
226-
font_size=60,
227-
stroke_width=1,
228-
line_count=1,
229-
ink_height=9999,
230-
)
231-
232-
self.assertEqual(single_line, ascent + descent + 2)
233-
self.assertEqual(two_lines, (ascent + descent) * 2 + 2)
234-
self.assertEqual(ink_taller_than_metrics, 9999)
235-
236-
def test_text_block_height_falls_back_when_font_is_unreadable(self):
237-
"""字体探测失败时不能让字幕高度归零,应至少退回墨迹高度。"""
238-
self.assertEqual(
239-
video._subtitle_text_block_height(
240-
font_path="/nonexistent/font.ttf",
241-
font_size=60,
242-
stroke_width=1,
243-
line_count=1,
244-
ink_height=88,
245-
),
246-
88,
247-
)

webui.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ export PYTHONPATH="$CURRENT_DIR${PYTHONPATH:+:$PYTHONPATH}"
1616
MPT_WEBUI_HOST="${MPT_WEBUI_HOST:-127.0.0.1}"
1717
MPT_WEBUI_PORT="${MPT_WEBUI_PORT:-8501}"
1818

19+
# macOS 上 Pillow 在运行时 dlopen libraqm/libfribidi,但 Homebrew 的动态库目录
20+
# 不在默认搜索路径里,Pillow 会直接报告 raqm=False,阿拉伯语等从右到左的字幕
21+
# 就不会连写、词序也会颠倒。这里在检测到 Homebrew 已安装相关动态库时补上
22+
# DYLD_LIBRARY_PATH,Intel 与 Apple Silicon 的默认前缀都会尝试。
23+
if [ "$(uname -s)" = "Darwin" ]; then
24+
for MPT_BREW_LIB in "$(brew --prefix 2>/dev/null)/lib" /opt/homebrew/lib /usr/local/lib; do
25+
if [ -e "$MPT_BREW_LIB/libraqm.dylib" ] || [ -e "$MPT_BREW_LIB/libfribidi.dylib" ]; then
26+
case ":${DYLD_LIBRARY_PATH:-}:" in
27+
*":$MPT_BREW_LIB:"*) ;;
28+
*) export DYLD_LIBRARY_PATH="$MPT_BREW_LIB${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" ;;
29+
esac
30+
break
31+
fi
32+
done
33+
unset MPT_BREW_LIB
34+
fi
35+
1936
if [ -x "$CURRENT_DIR/.venv/bin/python" ]; then
2037
PORT_CHECK_CMD="$CURRENT_DIR/.venv/bin/python"
2138
set -- "$CURRENT_DIR/.venv/bin/python" -m streamlit

webui/i18n/it.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
"Rounded Subtitle Background Help": "Disabilitato per impostazione predefinita. Se abilitato, lo sfondo dei sottotitoli utilizza uno stile arrotondato e traslucido che si adatta meglio alla larghezza del testo.",
176176
"Subtitle Colors Are Indistinguishable": "Il testo e lo sfondo dei sottotitoli usano lo stesso colore, quindi i sottotitoli potrebbero non essere visibili.",
177177
"Subtitle Font Does Not Support Text": "Il carattere selezionato per i sottotitoli non supporta alcuni caratteri del testo, quindi alcuni sottotitoli potrebbero mancare.",
178+
"Subtitle Text Shaping Unavailable": "I sottotitoli scritti da destra a sinistra come l'arabo richiedono il motore di composizione del testo Raqm, che qui non è disponibile: le lettere non verranno unite e l'ordine delle parole risulterà invertito. Installa libraqm (Debian/Ubuntu: libraqm0, macOS: brew install libraqm).",
178179
"Restore Default Subtitle Settings": "Ripristina valori predefiniti dei sottotitoli",
179180
"Default Subtitle Settings Restored": "Impostazioni predefinite dei sottotitoli ripristinate",
180181
"Generate Video": "Genera video",

0 commit comments

Comments
 (0)