diff --git a/app/models/schema.py b/app/models/schema.py index fb168b4..0689a41 100644 --- a/app/models/schema.py +++ b/app/models/schema.py @@ -99,24 +99,29 @@ class VideoParams(BaseModel): text_background_color: Union[bool, str] = True # 艺术字体相关参数 - art_font_enabled: Optional[bool] = False - art_font_type: Optional[str] = "normal" # normal, shadow, outline, 3d, etc. + art_font_enabled: Optional[bool] = True + art_font_type: Optional[str] = "3d" # normal, shadow, outline, 3d, etc. art_font_background: Optional[str] = "none" # none, red, blue, etc. + subtitle_position: Optional[str] = "custom" # top, bottom, center, custom + custom_position: float = 65.0 # 标题贴纸相关参数 - title_sticker_enabled: Optional[bool] = False + title_sticker_enabled: Optional[bool] = True title_sticker_text: Optional[str] = "" title_sticker_font: Optional[str] = "STHeitiMedium.ttc" - title_sticker_font_size: Optional[int] = 80 - title_sticker_style: Optional[str] = "rainbow" # rainbow, neon, gradient, etc. + title_sticker_font_size: Optional[int] = 160 + title_sticker_style: Optional[str] = "metallic" # rainbow, neon, gradient, metallic, etc. + title_sticker_background_enabled: Optional[bool] = True # 是否启用背景 title_sticker_background: Optional[str] = "rounded_rect" # none, rounded_rect, rect, etc. title_sticker_background_color: Optional[str] = "#000000" title_sticker_border: Optional[bool] = True title_sticker_border_color: Optional[str] = "#FFFFFF" + title_sticker_position: Optional[str] = "custom" # upper_middle, middle, lower_middle, custom + title_sticker_custom_position: Optional[float] = 15.0 # 自定义位置,表示离顶部的百分比 font_size: int = 60 - stroke_color: Optional[str] = "#000000" - stroke_width: float = 1.5 + stroke_color: Optional[str] = "#FFFFFF" + stroke_width: float = 4.0 n_threads: Optional[int] = 2 paragraph_number: Optional[int] = 1 diff --git a/app/services/video.py b/app/services/video.py index 8beca82..4c14653 100644 --- a/app/services/video.py +++ b/app/services/video.py @@ -183,7 +183,7 @@ def combine_videos( return combined_video_path -def create_title_sticker(text, font, font_size, style, background, background_color, border, border_color, size): +def create_title_sticker(text, font, font_size, style, background, background_color, border, border_color, size, background_enabled=True): """ 创建标题贴纸 @@ -196,6 +196,7 @@ def create_title_sticker(text, font, font_size, style, background, background_co :param border: 是否有边框 :param border_color: 边框颜色 :param size: 视频尺寸 + :param background_enabled: 是否启用背景 :return: ImageClip对象 """ if not text: @@ -224,8 +225,8 @@ def create_title_sticker(text, font, font_size, style, background, background_co img = Image.new('RGBA', (sticker_width, sticker_height), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) - # 绘制背景 - if background != "none": + # 绘制背景(如果启用了背景) + if background != "none" and background_enabled: # 确保背景颜色完全不透明 if background_color.startswith('#') and len(background_color) == 7: bg_color = background_color + 'ff' # 添加不透明度 @@ -719,15 +720,33 @@ def generate_video( background_color=params.title_sticker_background_color, border=params.title_sticker_border, border_color=params.title_sticker_border_color, - size=(video_width, video_height) + size=(video_width, video_height), + background_enabled=params.title_sticker_background_enabled ) - # 设置标题贴纸位置(顶部中间) + # 设置标题贴纸位置 if title_sticker: - title_sticker = title_sticker.with_position(("center", video_height * 0.05)) + # 根据用户选择的位置设置标题贴纸位置 + if params.title_sticker_position == "upper_middle": + # 上方中间 + title_sticker = title_sticker.with_position(("center", video_height * 0.10)) + elif params.title_sticker_position == "middle": + # 正中间 + title_sticker = title_sticker.with_position(("center", "center")) + elif params.title_sticker_position == "lower_middle": + # 下方中间 + title_sticker = title_sticker.with_position(("center", video_height * 0.80)) + elif params.title_sticker_position == "custom": + # 自定义位置 + custom_y = video_height * (params.title_sticker_custom_position / 100) + title_sticker = title_sticker.with_position(("center", custom_y)) + else: + # 默认位置(上方中间) + title_sticker = title_sticker.with_position(("center", video_height * 0.10)) + title_sticker = title_sticker.with_duration(video_clip.duration) video_elements.append(title_sticker) - logger.info(f"Added title sticker: {params.title_sticker_text}") + logger.info(f"Added title sticker: {params.title_sticker_text} at position {params.title_sticker_position}") # 添加字幕 if subtitle_path and os.path.exists(subtitle_path): @@ -819,6 +838,344 @@ def preprocess_video(materials: List[MaterialInfo], clip_duration=4): return materials +def create_preview_image(text, font_path, font_size, style, background_type, background_color, border, border_color, background_enabled=True, is_title=False): + """ + 创建预览图像 + + :param text: 文本内容 + :param font_path: 字体路径 + :param font_size: 字体大小 + :param style: 样式(normal, shadow, outline, 3d, neon, metallic, rainbow, gradient) + :param background_type: 背景类型(none, rounded_rect, rect等) + :param background_color: 背景颜色 + :param border: 是否有边框 + :param border_color: 边框颜色 + :param background_enabled: 是否启用背景 + :param is_title: 是否是标题(影响样式处理) + :return: 临时图像文件路径 + """ + # 设置预览图像宽度 + preview_width = 400 + + # 创建字体对象 + font_obj = ImageFont.truetype(font_path, font_size) + + # 计算文本尺寸 + left, top, right, bottom = font_obj.getbbox(text) + text_width = right - left + text_height = bottom - top + + # 设置图像尺寸(比文本略大) + padding_x = int(text_width * 0.3) + padding_y = int(text_height * 0.5) + img_width = min(preview_width, text_width + padding_x * 2) + img_height = text_height + padding_y * 2 + + # 确保文本在背景中垂直居中 + text_y_position = (img_height - text_height) // 2 + + # 创建透明背景图像 + img = Image.new('RGBA', (img_width, img_height), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + # 绘制背景(如果启用了背景) + if background_type != "none" and background_enabled: + # 确保背景颜色完全不透明 + if background_color.startswith('#') and len(background_color) == 7: + bg_color = background_color + 'ff' # 添加不透明度 + else: + bg_color = background_color + + if background_type == "rounded_rect": + # 绘制圆角矩形 + radius = int(img_height * 0.3) # 圆角半径 + draw.rounded_rectangle( + [(0, 0), (img_width, img_height)], + radius=radius, + fill=bg_color + ) + elif background_type == "rect": + # 绘制矩形 + draw.rectangle( + [(0, 0), (img_width, img_height)], + fill=bg_color + ) + + # 根据样式绘制文本 + if is_title and style == "rainbow": + # 彩虹渐变文字 + rainbow_colors = ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#9400D3"] + # 创建渐变色文本 + gradient_img = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0)) + gradient_draw = ImageDraw.Draw(gradient_img) + + # 计算每个字符的颜色 + for i, char in enumerate(text): + color_idx = i % len(rainbow_colors) + char_width = font_obj.getbbox(char)[2] - font_obj.getbbox(char)[0] + gradient_draw.text((left + i * char_width, 0), char, font=font_obj, fill=rainbow_colors[color_idx]) + + # 添加描边 + if border: + for offset_x, offset_y in [(1, 0), (-1, 0), (0, 1), (0, -1)]: + draw.text((padding_x + offset_x, text_y_position + offset_y), text, font=font_obj, fill=border_color) + + # 将渐变文本粘贴到主图像 + img.paste(gradient_img, (padding_x, text_y_position), gradient_img) + + elif is_title and style == "neon": + # 霓虹灯效果 + glow_color = "#FF4500" # 橙红色 + outer_glow_color = "#FFFF00" # 黄色外发光 + + # 添加外发光效果 + for offset in range(3, 0, -1): + alpha = 100 - offset * 30 + glow_alpha = max(0, alpha) + glow_color_with_alpha = glow_color[0:7] + format(glow_alpha, '02x') + for dx, dy in [(ox, oy) for ox in range(-offset, offset+1) for oy in range(-offset, offset+1)]: + draw.text((padding_x + dx, text_y_position + dy), text, font=font_obj, fill=glow_color_with_alpha) + + # 添加内发光 + draw.text((padding_x, text_y_position), text, font=font_obj, fill=outer_glow_color) + + # 添加主文本 + draw.text((padding_x, text_y_position), text, font=font_obj, fill=glow_color) + + # 应用模糊效果增强霓虹感 + img = img.filter(ImageFilter.GaussianBlur(1)) + + elif is_title and style == "gradient": + # 渐变效果 + start_color = (255, 0, 0) # 红色 + end_color = (0, 0, 255) # 蓝色 + + # 创建渐变色文本 + gradient_img = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0)) + gradient_draw = ImageDraw.Draw(gradient_img) + + # 绘制渐变背景 + for y in range(text_height): + r = int(start_color[0] + (end_color[0] - start_color[0]) * y / text_height) + g = int(start_color[1] + (end_color[1] - start_color[1]) * y / text_height) + b = int(start_color[2] + (end_color[2] - start_color[2]) * y / text_height) + gradient_draw.line([(0, y), (text_width, y)], fill=(r, g, b, 255)) + + # 创建文本蒙版 + mask = Image.new('L', (text_width, text_height), 0) + mask_draw = ImageDraw.Draw(mask) + mask_draw.text((0, 0), text, font=font_obj, fill=255) + + # 应用蒙版到渐变图像 + gradient_text = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0)) + gradient_text.paste(gradient_img, (0, 0), mask) + + # 添加描边 + if border: + for offset_x, offset_y in [(1, 0), (-1, 0), (0, 1), (0, -1)]: + draw.text((padding_x + offset_x, text_y_position + offset_y), text, font=font_obj, fill=border_color) + + # 将渐变文本粘贴到主图像 + img.paste(gradient_text, (padding_x, text_y_position), gradient_text) + + elif style == "shadow": + # 阴影效果 + shadow_offset = max(2, font_size // 20) # 阴影偏移量 + draw.text((padding_x + shadow_offset, text_y_position + shadow_offset), text, font=font_obj, fill=(0, 0, 0, 160)) + draw.text((padding_x, text_y_position), text, font=font_obj, fill="#FFFFFF") + + elif style == "outline": + # 描边效果 + outline_size = max(2, font_size // 25) # 描边大小 + # 绘制描边(四个方向) + for dx, dy in [(-1,-1), (-1,1), (1,-1), (1,1), (-outline_size,0), (outline_size,0), (0,-outline_size), (0,outline_size)]: + draw.text((padding_x + dx, text_y_position + dy), text, font=font_obj, fill=(0, 0, 0, 200)) + # 绘制主文本 + draw.text((padding_x, text_y_position), text, font=font_obj, fill="#FFFFFF") + + elif style == "3d": + # 3D立体效果 + depth = max(3, font_size // 15) # 3D深度 + for i in range(depth, 0, -1): + alpha = 100 + (155 * i // depth) # 渐变透明度 + shadow_color = (0, 0, 0, alpha) + draw.text((padding_x - i, text_y_position + i), text, font=font_obj, fill=shadow_color) + # 绘制主文本 + draw.text((padding_x, text_y_position), text, font=font_obj, fill="#FFFFFF") + + elif style == "metallic": + # 金属效果 + # 金属渐变色 + metallic_base = (212, 175, 55, 255) # 金色基色 + + # 绘制金属效果的底色 + draw.text((padding_x, text_y_position), text, font=font_obj, fill=metallic_base) + + # 添加高光效果 + highlight_offset = max(1, font_size // 30) + draw.text((padding_x - highlight_offset, text_y_position - highlight_offset), + text, font=font_obj, fill=(255, 255, 255, 100)) + + # 添加阴影增强金属感 + shadow_offset = max(1, font_size // 25) + draw.text((padding_x + shadow_offset, text_y_position + shadow_offset), + text, font=font_obj, fill=(100, 100, 100, 100)) + + else: # normal + # 添加描边 + if border: + for offset_x, offset_y in [(1, 0), (-1, 0), (0, 1), (0, -1)]: + draw.text((padding_x + offset_x, text_y_position + offset_y), text, font=font_obj, fill=border_color) + + # 绘制主文本 + draw.text((padding_x, text_y_position), text, font=font_obj, fill="#FFFFFF") + + # 保存为临时文件 + temp_img_path = os.path.join(utils.storage_dir("temp", create=True), f"preview_{str(uuid.uuid4())}.png") + img.save(temp_img_path, format="PNG") + + return temp_img_path + + +def create_unified_preview(video_aspect, subtitle_params=None, title_params=None): + """ + 创建统一预览图像 + + :param video_aspect: 视频宽高比 + :param subtitle_params: 字幕参数字典 + :param title_params: 标题参数字典 + :return: 临时图像文件路径 + """ + # 获取视频尺寸 + aspect = VideoAspect(video_aspect) + video_width, video_height = aspect.to_resolution() + + # 计算预览区域的尺寸,保持原始宽高比 + preview_width = 400 # 固定宽度 + preview_height = int(preview_width * video_height / video_width) + + # 创建背景图像 + bg_img = Image.new('RGB', (preview_width, preview_height), (51, 51, 51)) # #333333 背景色 + + # 如果有字幕参数,创建字幕预览 + if subtitle_params and subtitle_params.get("enabled", False): + # 获取字幕参数 + text = subtitle_params.get("text", "字幕预览") + font_path = subtitle_params.get("font_path") + font_size = subtitle_params.get("font_size", 60) + style = subtitle_params.get("style", "normal") + background = subtitle_params.get("background", "none") + background_color = subtitle_params.get("background_color", "#000000") + border = subtitle_params.get("border", True) + border_color = subtitle_params.get("border_color", "#FFFFFF") + position = subtitle_params.get("position", "bottom") + custom_position = subtitle_params.get("custom_position", 85.0) + background_enabled = subtitle_params.get("background_enabled", True) + + # 创建字幕预览图像 + subtitle_img_path = create_preview_image( + text=text, + font_path=font_path, + font_size=font_size, + style=style, + background_type=background, + background_color=background_color, + border=border, + border_color=border_color, + background_enabled=background_enabled, + is_title=False + ) + + # 加载字幕图像 + subtitle_img = Image.open(subtitle_img_path) + + # 计算字幕位置 + if position == "top": + subtitle_y = int(preview_height * 0.05) + elif position == "center": + subtitle_y = int(preview_height * 0.5 - subtitle_img.height / 2) + elif position == "bottom": + subtitle_y = int(preview_height * 0.95 - subtitle_img.height) + elif position == "custom": + subtitle_y = int(preview_height * custom_position / 100 - subtitle_img.height / 2) + else: + subtitle_y = int(preview_height * 0.95 - subtitle_img.height) + + # 计算水平居中位置 + subtitle_x = int(preview_width / 2 - subtitle_img.width / 2) + + # 将字幕图像粘贴到背景图像上 + bg_img.paste(subtitle_img, (subtitle_x, subtitle_y), subtitle_img) + + # 删除临时文件 + try: + os.remove(subtitle_img_path) + except Exception as e: + logger.warning(f"Failed to remove temporary subtitle image file: {e}") + + # 如果有标题参数,创建标题预览 + if title_params and title_params.get("enabled", False): + # 获取标题参数 + text = title_params.get("text", "标题预览") + font_path = title_params.get("font_path") + font_size = title_params.get("font_size", 80) + style = title_params.get("style", "rainbow") + background = title_params.get("background", "rounded_rect") + background_color = title_params.get("background_color", "#000000") + border = title_params.get("border", True) + border_color = title_params.get("border_color", "#FFFFFF") + position = title_params.get("position", "upper_middle") + custom_position = title_params.get("custom_position", 15.0) + background_enabled = title_params.get("background_enabled", True) + + # 创建标题预览图像 + title_img_path = create_preview_image( + text=text, + font_path=font_path, + font_size=font_size, + style=style, + background_type=background, + background_color=background_color, + border=border, + border_color=border_color, + background_enabled=background_enabled, + is_title=True + ) + + # 加载标题图像 + title_img = Image.open(title_img_path) + + # 计算标题位置 + if position == "upper_middle": + title_y = int(preview_height * 0.10 - title_img.height / 2) + elif position == "middle": + title_y = int(preview_height * 0.5 - title_img.height / 2) + elif position == "lower_middle": + title_y = int(preview_height * 0.80 - title_img.height / 2) + elif position == "custom": + title_y = int(preview_height * custom_position / 100 - title_img.height / 2) + else: + title_y = int(preview_height * 0.10 - title_img.height / 2) + + # 计算水平居中位置 + title_x = int(preview_width / 2 - title_img.width / 2) + + # 将标题图像粘贴到背景图像上 + bg_img.paste(title_img, (title_x, title_y), title_img) + + # 删除临时文件 + try: + os.remove(title_img_path) + except Exception as e: + logger.warning(f"Failed to remove temporary title image file: {e}") + + # 保存最终预览图像 + preview_img_path = os.path.join(utils.storage_dir("temp", create=True), f"unified_preview_{str(uuid.uuid4())}.png") + bg_img.save(preview_img_path, format="PNG") + + return preview_img_path + + if __name__ == "__main__": m = MaterialInfo() m.url = "/Users/harry/Downloads/IMG_2915.JPG" diff --git a/resource/fonts/AlimamaFangYuanTiVF-Thin-2.ttf b/resource/fonts/AlimamaFangYuanTiVF-Thin-2.ttf new file mode 100644 index 0000000..d7dc773 Binary files /dev/null and b/resource/fonts/AlimamaFangYuanTiVF-Thin-2.ttf differ diff --git a/resource/fonts/GongFanMianFeiTi2.0(GengDuoZiTiBaiDuSouShiJueFang)-2.ttf b/resource/fonts/GongFanMianFeiTi2.0(GengDuoZiTiBaiDuSouShiJueFang)-2.ttf new file mode 100644 index 0000000..2b8b0ce Binary files /dev/null and b/resource/fonts/GongFanMianFeiTi2.0(GengDuoZiTiBaiDuSouShiJueFang)-2.ttf differ diff --git a/resource/fonts/JingNanBoBoHei-Bold-2.ttf b/resource/fonts/JingNanBoBoHei-Bold-2.ttf new file mode 100644 index 0000000..b9494f3 Binary files /dev/null and b/resource/fonts/JingNanBoBoHei-Bold-2.ttf differ diff --git a/resource/fonts/LogoSCUnboundedSans-Regular-2.ttf b/resource/fonts/LogoSCUnboundedSans-Regular-2.ttf new file mode 100644 index 0000000..8bf6dfa Binary files /dev/null and b/resource/fonts/LogoSCUnboundedSans-Regular-2.ttf differ diff --git a/resource/fonts/PingFangLaiJiangHuHuaiGuTi-2.ttf b/resource/fonts/PingFangLaiJiangHuHuaiGuTi-2.ttf new file mode 100644 index 0000000..9965a56 Binary files /dev/null and b/resource/fonts/PingFangLaiJiangHuHuaiGuTi-2.ttf differ diff --git a/resource/fonts/ZiKuJiangHuGuFengTi-2.ttf b/resource/fonts/ZiKuJiangHuGuFengTi-2.ttf new file mode 100644 index 0000000..703329e Binary files /dev/null and b/resource/fonts/ZiKuJiangHuGuFengTi-2.ttf differ diff --git a/resource/fonts/ZiKuXingQiuFeiYangTi-2.ttf b/resource/fonts/ZiKuXingQiuFeiYangTi-2.ttf new file mode 100644 index 0000000..441e948 Binary files /dev/null and b/resource/fonts/ZiKuXingQiuFeiYangTi-2.ttf differ diff --git a/resource/fonts/dingliehakkafont-2.ttf b/resource/fonts/dingliehakkafont-2.ttf new file mode 100644 index 0000000..35efd7a Binary files /dev/null and b/resource/fonts/dingliehakkafont-2.ttf differ diff --git a/resource/fonts/dingliehuobanfont20241217-2.ttf b/resource/fonts/dingliehuobanfont20241217-2.ttf new file mode 100644 index 0000000..ddb6065 Binary files /dev/null and b/resource/fonts/dingliehuobanfont20241217-2.ttf differ diff --git a/resource/fonts/dingliexidafont-20250329V2)-2.ttf b/resource/fonts/dingliexidafont-20250329V2)-2.ttf new file mode 100644 index 0000000..1f5aa9b Binary files /dev/null and b/resource/fonts/dingliexidafont-20250329V2)-2.ttf differ diff --git a/resource/fonts/huangkaihuaLawyerfont-2.ttf b/resource/fonts/huangkaihuaLawyerfont-2.ttf new file mode 100644 index 0000000..298e682 Binary files /dev/null and b/resource/fonts/huangkaihuaLawyerfont-2.ttf differ diff --git a/webui/Main.py b/webui/Main.py index d83b7fc..7615669 100644 --- a/webui/Main.py +++ b/webui/Main.py @@ -46,6 +46,7 @@ hide_streamlit_style = """ """ st.markdown(hide_streamlit_style, unsafe_allow_html=True) + st.title(f"MoneyPrinterTurbo v{config.project_version}") support_locales = [ @@ -66,15 +67,6 @@ config_file = os.path.join(root_dir, "webui", ".streamlit", "webui.toml") system_locale = utils.get_system_locale() # print(f"******** system locale: {system_locale} ********") -if "video_subject" not in st.session_state: - st.session_state["video_subject"] = "" -if "video_script" not in st.session_state: - st.session_state["video_script"] = "" -if "video_terms" not in st.session_state: - st.session_state["video_terms"] = "" -if "ui_language" not in st.session_state: - st.session_state["ui_language"] = config.ui.get("language", system_locale) - def get_all_fonts(): fonts = [] @@ -86,6 +78,16 @@ def get_all_fonts(): return fonts +if "video_subject" not in st.session_state: + st.session_state["video_subject"] = "" +if "video_script" not in st.session_state: + st.session_state["video_script"] = "" +if "video_terms" not in st.session_state: + st.session_state["video_terms"] = "" +if "ui_language" not in st.session_state: + st.session_state["ui_language"] = config.ui.get("language", system_locale) + + def get_all_songs(): songs = [] for root, dirs, files in os.walk(song_dir): @@ -126,6 +128,21 @@ def scroll_to_bottom(): st.markdown(js, unsafe_allow_html=True) +def load_font_css(): + """Load all fonts from the font directory and create CSS for them""" + css = "" + return css + + def init_log(): logger.remove() _lvl = "DEBUG" @@ -586,7 +603,7 @@ with middle_panel: params.video_aspect = VideoAspect(video_aspect_ratios[selected_index][1]) params.video_clip_duration = st.selectbox( - tr("Clip Duration"), options=[2, 3, 4, 5, 6, 7, 8, 9, 10], index=1 + tr("Clip Duration"), options=[2, 3, 4, 5, 6, 7, 8, 9, 10], index=3 ) params.video_count = st.selectbox( tr("Number of Videos Generated Simultaneously"), @@ -737,7 +754,7 @@ with right_panel: ] selected_index = st.selectbox( tr("Position"), - index=2, + index=3, # 默认选择自定义位置 options=range(len(subtitle_positions)), format_func=lambda x: subtitle_positions[x][0], ) @@ -745,7 +762,7 @@ with right_panel: if params.subtitle_position == "custom": custom_position = st.text_input( - tr("Custom Position (% from top)"), value="70.0" + tr("Custom Position (% from top)"), value="65.0" ) try: params.custom_position = float(custom_position) @@ -769,13 +786,13 @@ with right_panel: stroke_cols = st.columns([0.3, 0.7]) with stroke_cols[0]: - params.stroke_color = st.color_picker(tr("Stroke Color"), "#000000") + params.stroke_color = st.color_picker(tr("Stroke Color"), "#FFFFFF") with stroke_cols[1]: - params.stroke_width = st.slider(tr("Stroke Width"), 0.0, 10.0, 1.5) + params.stroke_width = st.slider(tr("Stroke Width"), 0.0, 10.0, 4.0) # 艺术字体设置 st.write(tr("Art Font Settings")) - params.art_font_enabled = st.checkbox(tr("Enable Art Font"), value=False) + params.art_font_enabled = st.checkbox(tr("Enable Art Font"), value=True) if params.art_font_enabled: art_font_types = [ @@ -788,7 +805,7 @@ with right_panel: ] selected_index = st.selectbox( tr("Art Font Type"), - index=0, + index=2, # 默认选择3D立体 options=range(len(art_font_types)), format_func=lambda x: art_font_types[x][0], ) @@ -855,18 +872,11 @@ with right_panel: elif params.art_font_type == "metallic": font_style = f"color: {params.text_fore_color}; font-weight: bold; background: -webkit-linear-gradient(#eee, #333); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: 2px 2px 4px rgba(0,0,0,0.3);" - # 生成HTML预览 - preview_html = f""" -
- {preview_text} -
- """ - - st.markdown(preview_html, unsafe_allow_html=True) + # 不再需要单独的预览代码,因为我们使用了统一预览区域 # 标题贴纸设置 st.write(tr("Title Sticker Settings")) - params.title_sticker_enabled = st.checkbox(tr("Enable Title Sticker"), value=False) + params.title_sticker_enabled = st.checkbox(tr("Enable Title Sticker"), value=True) if params.title_sticker_enabled: params.title_sticker_text = st.text_input(tr("Title Text"), value="") @@ -884,7 +894,7 @@ with right_panel: # 标题贴纸字体大小 saved_title_font_size = config.ui.get("title_sticker_font_size", 80) - params.title_sticker_font_size = st.slider(tr("Title Font Size"), 40, 200, saved_title_font_size, key="title_font_size") + params.title_sticker_font_size = st.slider(tr("Title Font Size"), 40, 200, 160, key="title_font_size") config.ui["title_sticker_font_size"] = params.title_sticker_font_size # 标题贴纸样式 @@ -893,34 +903,67 @@ with right_panel: (tr("Neon"), "neon"), (tr("Gradient"), "gradient"), (tr("Normal"), "normal"), + (tr("3D"), "3d"), + (tr("Metallic"), "metallic"), + (tr("Shadow"), "shadow"), + (tr("Outline"), "outline"), ] selected_style_index = st.selectbox( tr("Title Style"), - index=0, + index=5, # 默认选择金属质感 options=range(len(title_sticker_styles)), format_func=lambda x: title_sticker_styles[x][0], key="title_style_select" ) params.title_sticker_style = title_sticker_styles[selected_style_index][1] - # 标题贴纸背景 - title_sticker_backgrounds = [ - (tr("None"), "none"), - (tr("Rounded Rectangle"), "rounded_rect"), - (tr("Rectangle"), "rect"), + # 标题贴纸位置 + title_sticker_positions = [ + (tr("Upper Middle"), "upper_middle"), + (tr("Middle"), "middle"), + (tr("Lower Middle"), "lower_middle"), + (tr("Custom"), "custom"), ] - selected_bg_index = st.selectbox( - tr("Title Background"), - index=1, - options=range(len(title_sticker_backgrounds)), - format_func=lambda x: title_sticker_backgrounds[x][0], - key="title_bg_select" + selected_pos_index = st.selectbox( + tr("Title Position"), + index=3, # 默认选择自定义位置 + options=range(len(title_sticker_positions)), + format_func=lambda x: title_sticker_positions[x][0], + key="title_pos_select" ) - params.title_sticker_background = title_sticker_backgrounds[selected_bg_index][1] + params.title_sticker_position = title_sticker_positions[selected_pos_index][1] - # 标题贴纸背景颜色 - if params.title_sticker_background != "none": + # 如果选择自定义位置,显示位置设置滑块 + if params.title_sticker_position == "custom": + params.title_sticker_custom_position = st.slider( + tr("Custom Position (% from top)"), + 0.0, 100.0, 15.0, 1.0, + key="title_custom_pos" + ) + + # 标题贴纸背景启用选项 + params.title_sticker_background_enabled = st.checkbox(tr("Enable Title Background"), value=True, key="title_bg_enabled") + + if params.title_sticker_background_enabled: + # 标题贴纸背景类型 + title_sticker_backgrounds = [ + (tr("Rounded Rectangle"), "rounded_rect"), + (tr("Rectangle"), "rect"), + ] + selected_bg_index = st.selectbox( + tr("Title Background Type"), + index=0, + options=range(len(title_sticker_backgrounds)), + format_func=lambda x: title_sticker_backgrounds[x][0], + key="title_bg_select" + ) + params.title_sticker_background = title_sticker_backgrounds[selected_bg_index][1] + + # 标题贴纸背景颜色 params.title_sticker_background_color = st.color_picker(tr("Title Background Color"), "#000000", key="title_bg_color") + else: + # 如果不启用背景,设置为无背景 + params.title_sticker_background = "none" # 标题贴纸边框 params.title_sticker_border = st.checkbox(tr("Enable Title Border"), value=True, key="title_border") @@ -936,7 +979,7 @@ with right_panel: title_bg_style = "" # 设置背景样式 - if params.title_sticker_background != "none": + if params.title_sticker_background_enabled and params.title_sticker_background != "none": if params.title_sticker_background == "rounded_rect": title_bg_style = f"background-color: {params.title_sticker_background_color}; border-radius: 15px;" else: @@ -954,17 +997,84 @@ with right_panel: title_font_style = "color: #FF4500; font-weight: bold; text-shadow: 0 0 5px #FFFF00, 0 0 10px #FFFF00, 0 0 15px #FF4500, 0 0 20px #FF4500;" elif params.title_sticker_style == "gradient": title_font_style = "background: linear-gradient(to bottom, #ff0000, #0000ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: bold;" + elif params.title_sticker_style == "3d": + title_font_style = "color: #FFFFFF; font-weight: bold; text-shadow: 0px 1px 0px #999, 0px 2px 0px #888, 0px 3px 0px #777, 0px 4px 0px #666, 0px 5px 0px #555, 0px 6px 0px #444, 0px 7px 0px #333, 0px 8px 7px #001135;" + elif params.title_sticker_style == "metallic": + title_font_style = "color: #D4AF37; font-weight: bold; background: -webkit-linear-gradient(#eee, #D4AF37); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: 2px 2px 4px rgba(0,0,0,0.3);" + elif params.title_sticker_style == "shadow": + title_font_style = "color: #FFFFFF; font-weight: bold; text-shadow: 3px 3px 5px #000;" + elif params.title_sticker_style == "outline": + title_font_style = "color: #FFFFFF; font-weight: bold; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;" else: # normal title_font_style = "color: #FFFFFF; font-weight: bold;" - # 生成HTML预览 - title_preview_html = f""" -
- {title_preview_text} -
- """ + # 不再需要单独的预览代码,因为我们使用了统一预览区域 - st.markdown(f"
{title_preview_html}
", unsafe_allow_html=True) +# 添加统一预览区域 +# 在生成视频按钮之前显示统一预览区域 +with st.container(border=True): + st.write(tr("Video Preview")) + + # 准备字幕参数 + subtitle_params = None + if params.subtitle_enabled: + # 获取字幕字体路径 + subtitle_font_path = os.path.join(font_dir, params.font_name) + + # 准备字幕参数 + subtitle_params = { + "enabled": params.subtitle_enabled, + "text": "字幕预览", + "font_path": subtitle_font_path, + "font_size": params.font_size, + "style": params.art_font_type if params.art_font_enabled else "normal", + "background": "rounded_rect" if params.art_font_background != "none" else "none", + "background_color": params.art_font_background if params.art_font_background != "none" else "#000000", + "border": True, + "border_color": params.stroke_color, + "position": params.subtitle_position, + "custom_position": params.custom_position, + "background_enabled": params.art_font_background != "none" + } + + # 准备标题参数 + title_params = None + if params.title_sticker_enabled: + # 获取标题字体路径 + title_font_path = os.path.join(font_dir, params.title_sticker_font) + + # 准备标题参数 + title_params = { + "enabled": params.title_sticker_enabled, + "text": params.title_sticker_text or "标题预览", + "font_path": title_font_path, + "font_size": params.title_sticker_font_size, + "style": params.title_sticker_style, + "background": params.title_sticker_background, + "background_color": params.title_sticker_background_color, + "border": params.title_sticker_border, + "border_color": params.title_sticker_border_color, + "position": params.title_sticker_position, + "custom_position": params.title_sticker_custom_position, + "background_enabled": params.title_sticker_background_enabled + } + + # 生成预览图像 + from app.services.video import create_unified_preview + preview_img_path = create_unified_preview( + video_aspect=params.video_aspect, + subtitle_params=subtitle_params, + title_params=title_params + ) + + # 显示预览图像 + st.image(preview_img_path, use_column_width=True) + + # 删除临时文件 + try: + os.remove(preview_img_path) + except Exception as e: + logger.warning(f"Failed to remove temporary preview image file: {e}") start_button = st.button(tr("Generate Video"), use_container_width=True, type="primary") if start_button: diff --git a/webui/i18n/zh.json b/webui/i18n/zh.json index 846c4bd..df377a0 100644 --- a/webui/i18n/zh.json +++ b/webui/i18n/zh.json @@ -113,12 +113,22 @@ "Rainbow": "彩虹", "Neon": "霓虹灯", "Gradient": "渐变", + "Title Position": "标题位置", + "Upper Middle": "上方中间", + "Middle": "正中间", + "Lower Middle": "下方中间", + "Custom": "自定义", + "Custom Position (% from top)": "自定义位置(距离顶部百分比)", + "Enable Title Background": "启用标题背景", + "Title Background Type": "标题背景类型", "Title Background": "标题背景", "Rounded Rectangle": "圆角矩形", "Rectangle": "矩形", "Title Background Color": "标题背景颜色", "Enable Title Border": "启用标题边框", "Title Border Color": "标题边框颜色", - "Title Sticker Preview": "标题贴纸预览" + "Title Sticker Preview": "标题贴纸预览", + "Font Preview": "字体预览", + "Video Preview": "视频预览" } } \ No newline at end of file