From 99fcd45704f574121ac2f9ef11389cec7c450e20 Mon Sep 17 00:00:00 2001 From: viccy Date: Wed, 10 Jun 2026 12:02:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(subtitle,=20ui):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=AD=97=E5=B9=95=E5=AE=89=E5=85=A8=E5=8C=BA=E9=A2=84=E8=A7=88?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=AD=97=E4=BD=93=E4=B8=8E=E5=AD=97?= =?UTF-8?q?=E5=B9=95=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增竖屏/横屏字幕安全区预览背景图,支持切换预览比例 - 将项目版本从0.8.1升级至0.8.2 - 扩展字体搜索候选列表,新增SourceHanSerifSC-SemiBold.otf和LXGWWenKaiScreen.ttf两款字体 - 修改默认字幕字体为SourceHanSansCN-Regular.otf,替换原Microsoft YaHei默认值 - 新增内置字体检测逻辑,检测到resource/fonts目录有有效字体时跳过下载 - 更新中英文多语言文案,优化字幕位置提示文本 - 重构字幕设置面板,合并位置控制到预览区域并精简标签页 - 调整字体大小滑块范围从20-100扩展至20-160,新增数值边界校验 --- app/services/generate_video.py | 2 + app/utils/utils.py | 13 ++ project_version | 2 +- .../subtitle_safe_area_landscape.png | Bin 0 -> 20409 bytes .../subtitle_safe_area_portrait.png | Bin 0 -> 22250 bytes webui.py | 2 +- webui/components/subtitle_settings.py | 157 +++++++++++++++++- webui/i18n/en.json | 6 +- webui/i18n/zh.json | 6 +- 9 files changed, 175 insertions(+), 13 deletions(-) create mode 100644 resource/safe_areas/subtitle_safe_area_landscape.png create mode 100644 resource/safe_areas/subtitle_safe_area_portrait.png diff --git a/app/services/generate_video.py b/app/services/generate_video.py index 1fe41fd..f227b93 100644 --- a/app/services/generate_video.py +++ b/app/services/generate_video.py @@ -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", diff --git a/app/utils/utils.py b/app/utils/utils.py index 19dd46f..4ee1291 100644 --- a/app/utils/utils.py +++ b/app/utils/utils.py @@ -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", diff --git a/project_version b/project_version index c18d72b..53a48a1 100644 --- a/project_version +++ b/project_version @@ -1 +1 @@ -0.8.1 \ No newline at end of file +0.8.2 \ No newline at end of file diff --git a/resource/safe_areas/subtitle_safe_area_landscape.png b/resource/safe_areas/subtitle_safe_area_landscape.png new file mode 100644 index 0000000000000000000000000000000000000000..3daa804439af02e13f3d9b88d5344a6edc8299bd GIT binary patch literal 20409 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

IdKqteS+nD^xKzZAK|pj zng}*_a_sBlBY`(dq4j*?f+sZOfUdqV1jpKG#4lEDqo(K*#W8ir7vi}FAq-b=3+}+I zOdW7!SVcL*sc~l$BdMHlf-i;qj`C>{t=k>}8al2&LlU?+^^{_wFV6INmp^xspr%hS z$}RkQ+4zw41_!GJ9TI|C^j80(9HR3V84eP%=2$x=I%s`cx|^N|nJ%Sd-$t^Tu-#UY z!c=U8RLg?MN=4`$r6#bDMf7yJ6QGStZqe_dG+c!FRAjvb!%#A`jiD8^@^G zq02kH=7D7g4Oo(I`Qi}qOS}vu%;>}Oj{(N%m9p}5Q5n9Cwn#)LwyllVJ#Cd#ge-W= zaa%R7$7>M2eaS`^4R{zkRn+>XO1DabBI)&VHypkG3Q`;+6a`q>U{1;_mXD}b{(N2* z-9q=IBr0ul2jVm=GUtcz4FQV)x4`1)uBk0`3x-y} zjS~`fMvi4+Fupy|87-RD-P8px%RZgrY6Xv9xR%0~ZfkfwRB(l({-!XX2R})0Vv^}` zCJRlu_p9uj@BhYK62RIUgzEl?k;&FWIO85Txhq8QAr!9dE#HOMV$$+OZ9ZLWL(j?@ z6@8Q|U|(2dpZ&|3dhkiMWt}<@?$9Mq`$wc`T2`^uU=tW(e5VTiFmsr7OmvZ9^?>wq zj=)|8gi`Z2o1}9=E^A9tn#SYHwY)g`w`n?W*irn1idn5f;PVvR4q_7-q0JS!QMa(Fa z&7j$GqROH(Ijsb|{4YW~mSti6Ar#L97k&b|nY3uL@gq8OGc!=>^kz}eG8tNkUl@A@ zU8a#y4xu5T?bRQ_lVv1~P1Yf+HA0NWz(@XGd7CePJxyckaQMI{(0BS`x?43qKQ}^M zc<>K;wT9aIs)+B6Li;@BcC;Zwvh5B#EmWydYT2Z$4b>3O{Q=5{LgoL-afiDOl{W5} zsAl7S|4C9fN=DWAzYS#xKS-x#r>M3?ir7wAZ~$IwHeOT~03c+F1*Sh+L> z0$7@$SsE8wnvGhFOn{|9FeSe~npheHTN(uWZw!JhO-0bNQ!|6pM$0bNQ!myWFc!NSt{yrr{^|C61KrBka*M|A(w zu3F;8OWgQ>k{kCAW9IQxvAhkwR^tIC2V*&$@}JU)WE{%29QK@;h#7=(CDbFsn*YH{ z)vx^utN;Ayzk0}j0@GW#Y_t5qw~h(Hp;!(1F`zYi|5kOkEBzayZvE@D?yjl&kEm-E z|K)PF|JKrvtN8u78Ly)7@5U}Y46vj#0AR^90D%A3;pDekxq(*JR6(I!_ziO>d$;5H IwthGN5BnF@lLm0Bv-!uNEwfXLy z$94h$fZZ0?uiORz#0&rc$tT+-gd=C&>B0|oUU*<;b`yHr+*HZps+pdap`M<)wuTk} za6LZDHq5~`?65J1I{)KF+>?ZoRKIAz3FZ0YwkcnlPF^~4{Dl}rbl<_kQ)j*%xQg9& zs4(St=!usxPrr;+C|$ks_175@;CPz<4&EFv(gMszAIf(8Znh#Wr=d#>0Td~~e)Xwcgt1*3G8%7Ar~mP?I0U+Sxd+0Z4aLBiBl@&bGFPpHx(%DOF z9k{}Bo6a*5-+Z3A^34}uAOjejHu2c?TjVZzn-7Jn?DbvtlA+?qT+S<`!^;SZ_iZPO z^z^E$s|QXTr`x%ZRlr;7Io2oLK_lF=$5k(?TZt+CY zR=-}(P2Dj_9DU+>Qv^iG4dRE?F{^HIvvZ3$e>_+vVMr(&H375*$vjw@ZGA z|9j&2^^^NiCy$?wJCdy2{#y6?zS~dlD8XM}m~@&v6>_Zl$=Y%K$#s{y*qjj_MUq1rqERHpV~K#t=m>SLbR$=DAtQAsMo5eu?K8l z8mXkex#nnfI_LcDuG@<@(Y6J5M6tVcpPup^B$F&Wt zku1rSPea%U`(63>vkzv!$CZQT3J2a6bQA>GuG{X)S3tu+vZRXcq<&W{-oDleZfb7d z0x|z&TLisEkJkW^ZS}3fG8T7?Y5`wRErH2YJ^Wi3YLMZl`B7DOy8mo|(RNjaBV`-^UkN z6t-P>Yxg!b?^s@R9>`MGl9|@id9mYAIw{RXM>P%F(bpN44ym}LE~kE5{fs(H{T4_S zlsskKPI&P5fkt0bpKqQ^lb3%cF_GMGsAw=hSwH8GMrv%Cbfa5VS}v0`e!R2Rl+J`bu7KN zd-r!8d8{8quVw}-X11X}oQHBcE=wb%(dLOQ2pcn-Xu2`+mL|0Ld~uAc4c;H0OYNi< zR~1pFD7MCFsCt`h8&q*R(~q{321iODPa}6DCj?0X5Z^|8O!TQ}g_yH+|Bgw?0r4bB zLrHaMhE$%oxwyR;O}cb@@{8}WpRzA!JDD*%w9zVP(lH75Xm_W^#TOYbG^I9;pYGkK zaO>!@Qm;mc<30NW%D%`ui2imT=FFd&k+M#&cJHvJQ!OW(`+wEFWW}q;%O{k@J1K;n zc0KEQfR=K}GR|T!^_*5qSQpz+IX)>~JYE!PMb}7&yQ2r{5+YjYXNmMqlQ++7URq&o zU@f~QNK5}564Ur{_goi!C2UD?*G2IWF;CMl_xPamL%`~j>+Ub%zX#rx-a+;E_N$&K z`eM4Krmpn$!U7oPz!j{aj+P#^`tZ5qb4QMcZg5SN4oLsuEjvup?b}L6oo8x-ct3+5 zlstmyy#eaiS~b73xHlT2vwnB|W>-h%?@V&FR<-$|0*(Ee3|GI>@WwwqDo7B)^`+Bq z$Z)A;)DYvoSvx+3=@?z;Q1Zz$cl`Ipdu7~)y}W~!V-|rMdvT=hP2J ^~)zxyj<+ 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 literal 0 HcmV?d00001 diff --git a/webui.py b/webui.py index 7897fbb..571bc64 100644 --- a/webui.py +++ b/webui.py @@ -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'), diff --git a/webui/components/subtitle_settings.py b/webui/components/subtitle_settings.py index 2c0355d..a0dd270 100644 --- a/webui/components/subtitle_settings.py +++ b/webui/components/subtitle_settings.py @@ -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(): """获取字幕参数""" diff --git a/webui/i18n/en.json b/webui/i18n/en.json index 1ce0df5..5966da0 100644 --- a/webui/i18n/en.json +++ b/webui/i18n/en.json @@ -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...", diff --git a/webui/i18n/zh.json b/webui/i18n/zh.json index 321af09..ee20def 100644 --- a/webui/i18n/zh.json +++ b/webui/i18n/zh.json @@ -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": "正在生成视频,请稍候...",