feat(subtitle): 添加字幕自定义位置功能

- 在 generate_video.py 中实现自定义字幕位置的逻辑
- 在 schema.py 中添加 custom_position 字段
- 更新 webui 组件以支持自定义位置设置
- 调整任务处理逻辑,支持新的自定义位置参数
This commit is contained in:
linyq 2025-05-08 20:45:14 +08:00
parent b762bf8d93
commit fef3b3c8fd
4 changed files with 22 additions and 10 deletions

View File

@ -362,9 +362,10 @@ class VideoClipParams(BaseModel):
text_back_color: Optional[str] = None # 文本背景色 text_back_color: Optional[str] = None # 文本背景色
stroke_color: str = "black" # 描边颜色 stroke_color: str = "black" # 描边颜色
stroke_width: float = 1.5 # 描边宽度 stroke_width: float = 1.5 # 描边宽度
subtitle_position: str = "bottom" # top, bottom, center, custom subtitle_position: str = "bottom" # top, bottom, center, custom
custom_position: float = 70.0 # 自定义位置
n_threads: Optional[int] = Field(default=16, description="解说语音音量") # 线程<E7BABF><E7A88B><EFBFBD>,有助于提升视频处理速度 n_threads: Optional[int] = Field(default=16, description="线程数") # 线程数,有助于提升视频处理速度
tts_volume: Optional[float] = Field(default=1.0, description="解说语音音量(后处理)") tts_volume: Optional[float] = Field(default=1.0, description="解说语音音量(后处理)")
original_volume: Optional[float] = Field(default=1.0, description="视频原声音量") original_volume: Optional[float] = Field(default=1.0, description="视频原声音量")

View File

@ -10,7 +10,7 @@
import os import os
import traceback import traceback
from typing import Optional, Dict, Any, Union from typing import Optional, Dict, Any
from loguru import logger from loguru import logger
from moviepy import ( from moviepy import (
VideoFileClip, VideoFileClip,
@ -18,7 +18,6 @@ from moviepy import (
CompositeAudioClip, CompositeAudioClip,
CompositeVideoClip, CompositeVideoClip,
TextClip, TextClip,
concatenate_videoclips,
afx afx
) )
from moviepy.video.tools.subtitles import SubtitlesClip from moviepy.video.tools.subtitles import SubtitlesClip
@ -54,6 +53,7 @@ def merge_materials(
- subtitle_color: 字幕颜色默认白色 - subtitle_color: 字幕颜色默认白色
- subtitle_bg_color: 字幕背景颜色默认透明 - subtitle_bg_color: 字幕背景颜色默认透明
- subtitle_position: 字幕位置可选值'bottom', 'top', 'center'默认'bottom' - subtitle_position: 字幕位置可选值'bottom', 'top', 'center'默认'bottom'
- custom_position: 自定义位置
- stroke_color: 描边颜色默认黑色 - stroke_color: 描边颜色默认黑色
- stroke_width: 描边宽度默认1 - stroke_width: 描边宽度默认1
- threads: 处理线程数默认2 - threads: 处理线程数默认2
@ -71,11 +71,12 @@ def merge_materials(
bgm_volume = options.get('bgm_volume', 0.3) bgm_volume = options.get('bgm_volume', 0.3)
original_audio_volume = options.get('original_audio_volume', 0.0) # 默认为0即不保留原声 original_audio_volume = options.get('original_audio_volume', 0.0) # 默认为0即不保留原声
keep_original_audio = options.get('keep_original_audio', False) # 是否保留原声 keep_original_audio = options.get('keep_original_audio', False) # 是否保留原声
subtitle_font = options.get('subtitle_font', None) subtitle_font = options.get('subtitle_font', '')
subtitle_font_size = options.get('subtitle_font_size', 40) subtitle_font_size = options.get('subtitle_font_size', 40)
subtitle_color = options.get('subtitle_color', '#FFFFFF') subtitle_color = options.get('subtitle_color', '#FFFFFF')
subtitle_bg_color = options.get('subtitle_bg_color', 'transparent') subtitle_bg_color = options.get('subtitle_bg_color', 'transparent')
subtitle_position = options.get('subtitle_position', 'bottom') subtitle_position = options.get('subtitle_position', 'bottom')
custom_position = options.get('custom_position', 70)
stroke_color = options.get('stroke_color', '#000000') stroke_color = options.get('stroke_color', '#000000')
stroke_width = options.get('stroke_width', 1) stroke_width = options.get('stroke_width', 1)
threads = options.get('threads', 2) threads = options.get('threads', 2)
@ -222,6 +223,15 @@ def merge_materials(
_clip = _clip.with_position(("center", video_height * 0.95 - _clip.h)) _clip = _clip.with_position(("center", video_height * 0.95 - _clip.h))
elif subtitle_position == "top": elif subtitle_position == "top":
_clip = _clip.with_position(("center", video_height * 0.05)) _clip = _clip.with_position(("center", video_height * 0.05))
elif subtitle_position == "custom":
margin = 10
max_y = video_height - _clip.h - margin
min_y = margin
custom_y = (video_height - _clip.h) * (custom_position / 100)
custom_y = max(
min_y, min(custom_y, max_y)
)
_clip = _clip.with_position(("center", custom_y))
else: # center else: # center
_clip = _clip.with_position(("center", "center")) _clip = _clip.with_position(("center", "center"))

View File

@ -306,8 +306,8 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: di
output_video_path = path.join(utils.task_dir(task_id), f"combined.mp4") output_video_path = path.join(utils.task_dir(task_id), f"combined.mp4")
logger.info(f"\n\n## 6. 最后一步: 合并字幕/BGM/配音/视频 -> {output_video_path}") logger.info(f"\n\n## 6. 最后一步: 合并字幕/BGM/配音/视频 -> {output_video_path}")
bgm_path = '/Users/apple/Desktop/home/NarratoAI/resource/songs/bgm.mp3' # bgm_path = '/Users/apple/Desktop/home/NarratoAI/resource/songs/bgm.mp3'
# bgm_path = params.bgm_file bgm_path = utils.get_bgm_file()
# 调用示例 # 调用示例
options = { options = {
@ -315,11 +315,12 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: di
'bgm_volume': params.bgm_volume, # 背景音乐音量 'bgm_volume': params.bgm_volume, # 背景音乐音量
'original_audio_volume': params.original_volume, # 视频原声音量0表示不保留 'original_audio_volume': params.original_volume, # 视频原声音量0表示不保留
'keep_original_audio': True, # 是否保留原声 'keep_original_audio': True, # 是否保留原声
'subtitle_font': 'MicrosoftYaHeiNormal.ttc', # 这里使用相对字体路径,会自动在 font_dir() 目录下查找 'subtitle_font': params.font_name, # 这里使用相对字体路径,会自动在 font_dir() 目录下查找
'subtitle_font_size': params.font_size, 'subtitle_font_size': params.font_size,
'subtitle_color': '#FFFFFF', 'subtitle_color': params.text_fore_color,
'subtitle_bg_color': None, # 直接使用None表示透明背景 'subtitle_bg_color': None, # 直接使用None表示透明背景
'subtitle_position': params.subtitle_position, 'subtitle_position': params.subtitle_position,
'custom_position': params.custom_position,
'threads': params.n_threads 'threads': params.n_threads
} }
generate_video.merge_materials( generate_video.merge_materials(

View File

@ -127,7 +127,7 @@ def get_subtitle_params():
'font_name': st.session_state.get('font_name', ''), 'font_name': st.session_state.get('font_name', ''),
'font_size': st.session_state.get('font_size', 60), 'font_size': st.session_state.get('font_size', 60),
'text_fore_color': st.session_state.get('text_fore_color', '#FFFFFF'), 'text_fore_color': st.session_state.get('text_fore_color', '#FFFFFF'),
'position': st.session_state.get('subtitle_position', 'bottom'), 'subtitle_position': st.session_state.get('subtitle_position', 'bottom'),
'custom_position': st.session_state.get('custom_position', 70.0), 'custom_position': st.session_state.get('custom_position', 70.0),
'stroke_color': st.session_state.get('stroke_color', '#000000'), 'stroke_color': st.session_state.get('stroke_color', '#000000'),
'stroke_width': st.session_state.get('stroke_width', 1.5), 'stroke_width': st.session_state.get('stroke_width', 1.5),