Merge pull request #251 from linyqh/develop

feat(subtitle, ui): 新增字幕安全区预览,优化字体与字幕配置
This commit is contained in:
viccy 2026-06-10 12:16:37 +08:00 committed by GitHub
commit b9f07a6a10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 175 additions and 13 deletions

View File

@ -671,6 +671,8 @@ def _resolve_font_path(subtitle_font: str) -> Optional[str]:
for candidate in [
os.path.join(utils.font_dir(), "SourceHanSansCN-Regular.otf"),
os.path.join(utils.font_dir(), "SourceHanSerifSC-SemiBold.otf"),
os.path.join(utils.font_dir(), "LXGWWenKaiScreen.ttf"),
os.path.join(utils.font_dir(), "SimHei.ttf"),
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",

View File

@ -616,6 +616,19 @@ def init_resources():
font_dir = os.path.join(root_dir(), "resource", "fonts")
os.makedirs(font_dir, exist_ok=True)
existing_fonts = [
filename
for filename in os.listdir(font_dir)
if filename.lower().endswith((".ttf", ".ttc", ".otf"))
and os.path.isfile(os.path.join(font_dir, filename))
and os.path.getsize(os.path.join(font_dir, filename)) > 0
]
if existing_fonts:
if not getattr(init_resources, "_logged_existing_fonts_skip", False):
logger.info(f"已检测到内置字体文件,跳过字体下载: {', '.join(sorted(existing_fonts))}")
init_resources._logged_existing_fonts_skip = True
return
# 检查字体文件
font_files = [
("SourceHanSansCN-Regular.otf",

View File

@ -1 +1 @@
0.8.1
0.8.2

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -437,7 +437,7 @@ def get_jianying_export_params(draft_name=None) -> VideoClipParams:
n_threads=config.app.get('n_threads', 4),
video_aspect=VideoAspect.landscape,
subtitle_enabled=st.session_state.get('subtitle_enabled', False),
font_name=st.session_state.get('font_name', 'Microsoft YaHei'),
font_name=st.session_state.get('font_name', 'SourceHanSansCN-Regular.otf'),
font_size=st.session_state.get('font_size', 24),
text_fore_color=st.session_state.get('text_fore_color', '#FFFFFF'),
subtitle_position=st.session_state.get('subtitle_position', 'bottom'),

View File

@ -37,6 +37,16 @@ SUBTITLE_POSITION_DEFAULTS = {
VIDEO_PREVIEW_UPLOAD_TYPES = ["mp4", "mov", "avi", "flv", "mkv", "mpeg4"]
SUBTITLE_SAFE_AREA_PREVIEW_IMAGES = {
"portrait": "subtitle_safe_area_portrait.png",
"landscape": "subtitle_safe_area_landscape.png",
}
SUBTITLE_PREVIEW_FALLBACK_SIZES = {
"portrait": (1080, 1920),
"landscape": (1920, 1080),
}
def render_subtitle_panel(tr):
"""渲染字幕设置面板"""
@ -378,8 +388,9 @@ def _render_subtitle_mask_region_controls(tr, orientation):
def _render_subtitle_position_controls(tr, orientation):
label = tr("Portrait Subtitle Position") if orientation == "portrait" else tr("Landscape Subtitle Position")
y_percent = st.slider(
tr("Subtitle Burn Position"),
label,
min_value=0,
max_value=99,
value=int(_get_orientation_subtitle_position_value(orientation, "y_percent")),
@ -397,20 +408,14 @@ def _render_subtitle_mask_dialog(tr):
with settings_col:
st.caption(tr("Subtitle Mask Settings Caption"))
st.caption(tr("Subtitle Mask Preview Caption"))
landscape_mask_tab, portrait_mask_tab, landscape_position_tab, portrait_position_tab = st.tabs([
landscape_mask_tab, portrait_mask_tab = st.tabs([
tr("Landscape Subtitle Mask"),
tr("Portrait Subtitle Mask"),
tr("Landscape Subtitle Position"),
tr("Portrait Subtitle Position"),
])
with landscape_mask_tab:
_render_subtitle_mask_region_controls(tr, "landscape")
with portrait_mask_tab:
_render_subtitle_mask_region_controls(tr, "portrait")
with landscape_position_tab:
_render_subtitle_position_controls(tr, "landscape")
with portrait_position_tab:
_render_subtitle_position_controls(tr, "portrait")
with preview_col:
_render_subtitle_mask_preview(tr)
@ -604,10 +609,11 @@ def render_font_settings(tr):
with font_cols[1]:
saved_font_size = config.ui.get("font_size", 60)
saved_font_size = max(20, min(160, int(saved_font_size)))
font_size = st.slider(
tr("Font Size"),
min_value=20,
max_value=100,
max_value=160,
value=saved_font_size
)
config.ui["font_size"] = font_size
@ -653,6 +659,136 @@ def render_position_settings(tr):
st.error(tr("Please enter a valid number"))
def _resolve_subtitle_preview_font_path(font_name):
if not font_name:
return ""
font_name = str(font_name)
candidates = []
if os.path.isabs(font_name):
candidates.append(font_name)
candidates.extend(
[
os.path.join(utils.font_dir(), font_name),
os.path.join(utils.font_dir(), os.path.basename(font_name)),
]
)
for candidate in candidates:
if candidate and os.path.exists(candidate):
return candidate
return ""
def _load_subtitle_preview_font(font_name, font_size):
from PIL import ImageFont
font_path = _resolve_subtitle_preview_font_path(font_name)
font_size = max(12, int(round(font_size or 60)))
if font_path:
try:
return ImageFont.truetype(font_path, font_size)
except OSError:
pass
return ImageFont.load_default(size=font_size)
def _resolve_subtitle_preview_color(color, fallback):
from PIL import ImageColor
try:
value = ImageColor.getrgb(str(color or fallback).strip())
except ValueError:
value = ImageColor.getrgb(fallback)
if len(value) == 4:
return value
return (*value, 255)
def _get_subtitle_preview_y(image_height, text_height, orientation):
subtitle_position = st.session_state.get("subtitle_position", "bottom")
if subtitle_position == "top":
return max(12, round(image_height * 0.05))
if subtitle_position == "center":
return max(0, round((image_height - text_height) / 2))
y_percent = _get_orientation_subtitle_position_value(orientation, "y_percent")
if y_percent is None and subtitle_position == "custom":
y_percent = st.session_state.get("custom_position", 70.0)
try:
y_percent = max(0.0, min(100.0, float(y_percent)))
except (TypeError, ValueError):
y_percent = 85.0
return max(0, round((image_height - text_height) * y_percent / 100))
def _get_subtitle_preview_background(orientation):
from PIL import Image
image_path = os.path.join(
utils.resource_dir("safe_areas"),
SUBTITLE_SAFE_AREA_PREVIEW_IMAGES.get(orientation, SUBTITLE_SAFE_AREA_PREVIEW_IMAGES["portrait"]),
)
if os.path.exists(image_path):
return Image.open(image_path).convert("RGBA")
width, height = SUBTITLE_PREVIEW_FALLBACK_SIZES.get(orientation, SUBTITLE_PREVIEW_FALLBACK_SIZES["portrait"])
return Image.new("RGBA", (width, height), (19, 24, 35, 255))
def _render_subtitle_preview_image(tr):
from PIL import ImageDraw
font_name = st.session_state.get("font_name") or config.ui.get("font_name", "")
font_size = int(st.session_state.get("font_size", config.ui.get("font_size", 60)) or 60)
text_color = st.session_state.get("text_fore_color", config.ui.get("text_fore_color", "#FFFFFF"))
stroke_color = st.session_state.get("stroke_color", config.ui.get("stroke_color", "#000000"))
stroke_width = float(st.session_state.get("stroke_width", config.ui.get("stroke_width", 1.5)) or 0)
orientation = st.pills(
tr("Subtitle Preview Orientation"),
options=["portrait", "landscape"],
default=st.session_state.get("subtitle_preview_orientation", "portrait"),
format_func=lambda value: tr("Portrait Safe Area") if value == "portrait" else tr("Landscape Safe Area"),
key="subtitle_preview_orientation",
width="stretch",
) or "portrait"
_render_subtitle_position_controls(tr, orientation)
preview_text = tr("Subtitle Preview Sample Text")
image = _get_subtitle_preview_background(orientation)
draw = ImageDraw.Draw(image)
font = _load_subtitle_preview_font(font_name, font_size)
stroke_width_px = max(0, int(round(stroke_width)))
bbox = draw.textbbox((0, 0), preview_text, font=font, stroke_width=stroke_width_px)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = max(20, round((image.width - text_width) / 2))
y = _get_subtitle_preview_y(image.height, text_height, orientation)
margin = max(10, stroke_width_px * 2 + 6)
y = max(margin, min(y, image.height - text_height - margin))
draw.text(
(x - bbox[0], y - bbox[1]),
preview_text,
font=font,
fill=_resolve_subtitle_preview_color(text_color, "#FFFFFF"),
stroke_width=stroke_width_px,
stroke_fill=_resolve_subtitle_preview_color(stroke_color, "#000000"),
)
st.image(image.convert("RGB"), width="stretch", output_format="PNG")
def render_style_settings(tr):
"""渲染样式设置"""
stroke_cols = st.columns([0.3, 0.7])
@ -674,6 +810,9 @@ def render_style_settings(tr):
)
st.session_state['stroke_width'] = stroke_width
with st.expander(tr("Subtitle Preview"), expanded=False):
_render_subtitle_preview_image(tr)
def get_subtitle_params():
"""获取字幕参数"""

View File

@ -89,7 +89,7 @@
"Subtitle Mask Opacity": "Mask Strength",
"Subtitle Mask Opacity Help": "Mask blend strength. Higher values cover source subtitles more strongly.",
"Subtitle Burn Position": "Subtitle Position",
"Subtitle Burn Position Help": "New subtitle distance from the top edge as a frame percentage. The blue line in preview shows this position.",
"Subtitle Burn Position Help": "New subtitle distance from the top edge as a frame percentage. The safe-area preview updates in real time.",
"Subtitle Mask Preview": "Source Subtitle Mask Preview",
"Subtitle Mask Preview Caption": "Upload a source video for preview, or use the currently selected source video. Uploaded files here are only used for mask preview.",
"Upload Subtitle Mask Preview Video": "Upload Preview Source Video",
@ -113,6 +113,10 @@
"Font Color": "Subtitle Color",
"Stroke Color": "Stroke Color",
"Stroke Width": "Stroke Width",
"Subtitle Preview Orientation": "Preview Ratio",
"Portrait Safe Area": "Portrait Safe Area",
"Landscape Safe Area": "Landscape Safe Area",
"Subtitle Preview Sample Text": "Preview",
"Generate Video": "Generate Video",
"Video Script and Subject Cannot Both Be Empty": "Video Subject and Video Script cannot both be empty",
"Generating Video": "Generating video, please wait...",

View File

@ -77,7 +77,7 @@
"Subtitle Mask Opacity": "遮罩强度",
"Subtitle Mask Opacity Help": "遮罩融合强度,数值越高越容易遮住原字幕",
"Subtitle Burn Position": "字幕位置",
"Subtitle Burn Position Help": "新字幕距离画面顶部的百分比;预览中的蓝线表示当前字幕位置",
"Subtitle Burn Position Help": "新字幕距离画面顶部的百分比,可在下方安全区预览中实时查看位置",
"Subtitle Mask Preview": "原字幕遮罩预览",
"Subtitle Mask Preview Caption": "可上传一段原视频作为预览,也可直接使用当前已选择的原视频;上传内容仅用于预览遮罩位置。",
"Upload Subtitle Mask Preview Video": "上传预览原视频",
@ -101,6 +101,10 @@
"Font Color": "字幕颜色",
"Stroke Color": "描边颜色",
"Stroke Width": "描边粗细",
"Subtitle Preview Orientation": "预览比例",
"Portrait Safe Area": "竖屏安全区",
"Landscape Safe Area": "横屏安全区",
"Subtitle Preview Sample Text": "字幕预览效果",
"Generate Video": "生成视频",
"Video Script and Subject Cannot Both Be Empty": "视频主题 和 视频文案,不能同时为空",
"Generating Video": "正在生成视频,请稍候...",