mirror of
https://github.com/linyqh/NarratoAI.git
synced 2025-12-11 02:12:50 +00:00
feat(clip_video): 支持亚秒级视频裁剪
- 增加对带毫秒时间戳的支持 - 优化时间戳解析和格式化逻辑 - 调整FFmpeg命令以支持毫秒级精度 - 更新文件命名规则以适应新时间戳格式 - 修复相关服务和接口以确保兼容性
This commit is contained in:
parent
bc732c10fd
commit
bacc1adfad
@ -368,7 +368,7 @@ class VideoClipParams(BaseModel):
|
||||
|
||||
tts_volume: Optional[float] = Field(default=1.0, description="解说语音音量(后处理)")
|
||||
original_volume: Optional[float] = Field(default=1.0, description="视频原声音量")
|
||||
bgm_volume: Optional[float] = Field(default=0.6, description="背景音乐音量")
|
||||
bgm_volume: Optional[float] = Field(default=0.3, description="背景音乐音量")
|
||||
|
||||
|
||||
class VideoTranscriptionRequest(BaseModel):
|
||||
|
||||
@ -22,10 +22,10 @@ def parse_timestamp(timestamp: str) -> tuple:
|
||||
解析时间戳字符串,返回开始和结束时间
|
||||
|
||||
Args:
|
||||
timestamp: 格式为'HH:MM:SS-HH:MM:SS'的时间戳字符串
|
||||
timestamp: 格式为'HH:MM:SS-HH:MM:SS'或'HH:MM:SS,sss-HH:MM:SS,sss'的时间戳字符串
|
||||
|
||||
Returns:
|
||||
tuple: (开始时间, 结束时间) 格式为'HH:MM:SS'
|
||||
tuple: (开始时间, 结束时间) 格式为'HH:MM:SS'或'HH:MM:SS,sss'
|
||||
"""
|
||||
start_time, end_time = timestamp.split('-')
|
||||
return start_time, end_time
|
||||
@ -36,21 +36,40 @@ def calculate_end_time(start_time: str, duration: float, extra_seconds: float =
|
||||
根据开始时间和持续时间计算结束时间
|
||||
|
||||
Args:
|
||||
start_time: 开始时间,格式为'HH:MM:SS'
|
||||
start_time: 开始时间,格式为'HH:MM:SS'或'HH:MM:SS,sss'(带毫秒)
|
||||
duration: 持续时间,单位为秒
|
||||
extra_seconds: 额外添加的秒数,默认为1秒
|
||||
|
||||
Returns:
|
||||
str: 计算后的结束时间,格式为'HH:MM:SS'
|
||||
str: 计算后的结束时间,格式与输入格式相同
|
||||
"""
|
||||
h, m, s = map(int, start_time.split(':'))
|
||||
total_seconds = h * 3600 + m * 60 + s + duration + extra_seconds
|
||||
|
||||
# 检查是否包含毫秒
|
||||
has_milliseconds = ',' in start_time
|
||||
milliseconds = 0
|
||||
|
||||
if has_milliseconds:
|
||||
time_part, ms_part = start_time.split(',')
|
||||
h, m, s = map(int, time_part.split(':'))
|
||||
milliseconds = int(ms_part)
|
||||
else:
|
||||
h, m, s = map(int, start_time.split(':'))
|
||||
|
||||
# 转换为总毫秒数
|
||||
total_milliseconds = ((h * 3600 + m * 60 + s) * 1000 + milliseconds +
|
||||
int((duration + extra_seconds) * 1000))
|
||||
|
||||
# 计算新的时、分、秒、毫秒
|
||||
ms_new = total_milliseconds % 1000
|
||||
total_seconds = total_milliseconds // 1000
|
||||
h_new = int(total_seconds // 3600)
|
||||
m_new = int((total_seconds % 3600) // 60)
|
||||
s_new = int(total_seconds % 60)
|
||||
|
||||
return f"{h_new:02d}:{m_new:02d}:{s_new:02d}"
|
||||
|
||||
# 返回与输入格式一致的时间字符串
|
||||
if has_milliseconds:
|
||||
return f"{h_new:02d}:{m_new:02d}:{s_new:02d},{ms_new:03d}"
|
||||
else:
|
||||
return f"{h_new:02d}:{m_new:02d}:{s_new:02d}"
|
||||
|
||||
|
||||
def check_hardware_acceleration() -> Optional[str]:
|
||||
@ -144,24 +163,30 @@ def clip_video(
|
||||
result = {}
|
||||
|
||||
for item in tts_result:
|
||||
_id = item["_id"]
|
||||
_id = item.get("_id", item.get("timestamp", "unknown"))
|
||||
timestamp = item["timestamp"]
|
||||
start_time, _ = parse_timestamp(timestamp)
|
||||
|
||||
# 根据持续时间计算真正的结束时间(加上1秒余量)
|
||||
duration = item["duration"]
|
||||
calculated_end_time = calculate_end_time(start_time, duration)
|
||||
|
||||
# 转换为FFmpeg兼容的时间格式(逗号替换为点)
|
||||
ffmpeg_start_time = start_time.replace(',', '.')
|
||||
ffmpeg_end_time = calculated_end_time.replace(',', '.')
|
||||
|
||||
# 格式化输出文件名
|
||||
output_filename = f"vid-{start_time.replace(':', '-')}-{calculated_end_time.replace(':', '-')}.mp4"
|
||||
# 格式化输出文件名(使用连字符替代冒号和逗号)
|
||||
safe_start_time = start_time.replace(':', '-').replace(',', '-')
|
||||
safe_end_time = calculated_end_time.replace(':', '-').replace(',', '-')
|
||||
output_filename = f"vid_{safe_start_time}@{safe_end_time}.mp4"
|
||||
output_path = os.path.join(output_dir, output_filename)
|
||||
|
||||
# 构建FFmpeg命令
|
||||
ffmpeg_cmd = [
|
||||
"ffmpeg", "-y", *hwaccel_args,
|
||||
"-i", video_origin_path,
|
||||
"-ss", start_time,
|
||||
"-to", calculated_end_time,
|
||||
"-ss", ffmpeg_start_time,
|
||||
"-to", ffmpeg_end_time,
|
||||
"-c:v", "h264_videotoolbox" if hwaccel == "videotoolbox" else "libx264",
|
||||
"-c:a", "aac",
|
||||
"-strict", "experimental",
|
||||
@ -170,7 +195,7 @@ def clip_video(
|
||||
|
||||
# 执行FFmpeg命令
|
||||
try:
|
||||
logger.info(f"裁剪视频片段: {timestamp} -> {start_time}到{calculated_end_time}")
|
||||
logger.info(f"裁剪视频片段: {timestamp} -> {ffmpeg_start_time}到{ffmpeg_end_time}")
|
||||
# logger.debug(f"执行命令: {' '.join(ffmpeg_cmd)}")
|
||||
|
||||
process = subprocess.run(
|
||||
|
||||
@ -377,20 +377,18 @@ def validate_params(video_path, audio_path, output_file, params):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
task_id = "qyn2-2-demo"
|
||||
task_id = "demo"
|
||||
|
||||
# 提前裁剪是为了方便检查视频
|
||||
subclip_path_videos = {
|
||||
1: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/6e7e343c7592c7d6f9a9636b55000f23/vid-00-00-00-00-01-15.mp4',
|
||||
2: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/6e7e343c7592c7d6f9a9636b55000f23/vid-00-01-15-00-04-40.mp4',
|
||||
3: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/6e7e343c7592c7d6f9a9636b55000f23/vid-00-04-41-00-04-58.mp4',
|
||||
4: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/6e7e343c7592c7d6f9a9636b55000f23/vid-00-04-58-00-05-45.mp4',
|
||||
5: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/6e7e343c7592c7d6f9a9636b55000f23/vid-00-05-45-00-06-00.mp4',
|
||||
6: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/6e7e343c7592c7d6f9a9636b55000f23/vid-00-06-00-00-06-03.mp4',
|
||||
1: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/588c37cba4e2e62714c24c4c9054fc51/vid-00-00-00_000-00-00-27_000.mp4',
|
||||
2: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/588c37cba4e2e62714c24c4c9054fc51/vid-00-00-30_000-00-00-57_000.mp4',
|
||||
3: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/588c37cba4e2e62714c24c4c9054fc51/vid-00-01-00_000-00-01-27_000.mp4',
|
||||
4: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/588c37cba4e2e62714c24c4c9054fc51/vid-00-01-30_000-00-01-57_000.mp4',
|
||||
}
|
||||
|
||||
params = VideoClipParams(
|
||||
video_clip_json_path="/Users/apple/Desktop/home/NarratoAI/resource/scripts/demo.json",
|
||||
video_origin_path="/Users/apple/Desktop/home/NarratoAI/resource/videos/qyn2-2无片头片尾.mp4",
|
||||
video_clip_json_path="/Users/apple/Desktop/home/NarratoAI/resource/scripts/2025-0507-185159.json",
|
||||
video_origin_path="/Users/apple/Desktop/home/NarratoAI/resource/videos/test.mp4",
|
||||
)
|
||||
start_subclip(task_id, params, subclip_path_videos)
|
||||
|
||||
@ -21,16 +21,25 @@ def extract_timestamp_from_video_path(video_path: str) -> str:
|
||||
video_path: 视频文件路径
|
||||
|
||||
Returns:
|
||||
提取出的时间戳,格式为 'HH:MM:SS-HH:MM:SS'
|
||||
提取出的时间戳,格式为 'HH:MM:SS-HH:MM:SS' 或 'HH:MM:SS,sss-HH:MM:SS,sss'
|
||||
"""
|
||||
# 使用正则表达式从文件名中提取时间戳
|
||||
filename = os.path.basename(video_path)
|
||||
match = re.search(r'vid-(\d{2}-\d{2}-\d{2})-(\d{2}-\d{2}-\d{2})\.mp4', filename)
|
||||
|
||||
if match:
|
||||
|
||||
# 匹配新格式: vid_00-00-00-000@00-00-20-250.mp4
|
||||
match_new = re.search(r'vid_(\d{2})-(\d{2})-(\d{2})-(\d{3})@(\d{2})-(\d{2})-(\d{2})-(\d{3})\.mp4', filename)
|
||||
if match_new:
|
||||
# 提取并格式化时间戳(包含毫秒)
|
||||
start_h, start_m, start_s, start_ms = match_new.group(1), match_new.group(2), match_new.group(3), match_new.group(4)
|
||||
end_h, end_m, end_s, end_ms = match_new.group(5), match_new.group(6), match_new.group(7), match_new.group(8)
|
||||
return f"{start_h}:{start_m}:{start_s},{start_ms}-{end_h}:{end_m}:{end_s},{end_ms}"
|
||||
|
||||
# 匹配旧格式: vid-00-00-00-00-00-00.mp4
|
||||
match_old = re.search(r'vid-(\d{2}-\d{2}-\d{2})-(\d{2}-\d{2}-\d{2})\.mp4', filename)
|
||||
if match_old:
|
||||
# 提取并格式化时间戳
|
||||
start_time = match.group(1).replace('-', ':')
|
||||
end_time = match.group(2).replace('-', ':')
|
||||
start_time = match_old.group(1).replace('-', ':')
|
||||
end_time = match_old.group(2).replace('-', ':')
|
||||
return f"{start_time}-{end_time}"
|
||||
|
||||
return ""
|
||||
@ -41,7 +50,7 @@ def calculate_duration(timestamp: str) -> float:
|
||||
计算时间戳范围的持续时间(秒)
|
||||
|
||||
Args:
|
||||
timestamp: 格式为 'HH:MM:SS-HH:MM:SS' 的时间戳
|
||||
timestamp: 格式为 'HH:MM:SS-HH:MM:SS' 或 'HH:MM:SS,sss-HH:MM:SS,sss' 的时间戳
|
||||
|
||||
Returns:
|
||||
持续时间(秒)
|
||||
@ -49,13 +58,28 @@ def calculate_duration(timestamp: str) -> float:
|
||||
try:
|
||||
start_time, end_time = timestamp.split('-')
|
||||
|
||||
# 解析时间
|
||||
start_h, start_m, start_s = map(int, start_time.split(':'))
|
||||
end_h, end_m, end_s = map(int, end_time.split(':'))
|
||||
# 处理毫秒部分
|
||||
if ',' in start_time:
|
||||
start_parts = start_time.split(',')
|
||||
start_time_parts = start_parts[0].split(':')
|
||||
start_ms = float('0.' + start_parts[1]) if len(start_parts) > 1 else 0
|
||||
start_h, start_m, start_s = map(int, start_time_parts)
|
||||
else:
|
||||
start_h, start_m, start_s = map(int, start_time.split(':'))
|
||||
start_ms = 0
|
||||
|
||||
if ',' in end_time:
|
||||
end_parts = end_time.split(',')
|
||||
end_time_parts = end_parts[0].split(':')
|
||||
end_ms = float('0.' + end_parts[1]) if len(end_parts) > 1 else 0
|
||||
end_h, end_m, end_s = map(int, end_time_parts)
|
||||
else:
|
||||
end_h, end_m, end_s = map(int, end_time.split(':'))
|
||||
end_ms = 0
|
||||
|
||||
# 转换为秒
|
||||
start_seconds = start_h * 3600 + start_m * 60 + start_s
|
||||
end_seconds = end_h * 3600 + end_m * 60 + end_s
|
||||
start_seconds = start_h * 3600 + start_m * 60 + start_s + start_ms
|
||||
end_seconds = end_h * 3600 + end_m * 60 + end_s + end_ms
|
||||
|
||||
# 计算时间差(秒)
|
||||
return round(end_seconds - start_seconds, 2)
|
||||
@ -177,51 +201,51 @@ if __name__ == '__main__':
|
||||
list_script = [
|
||||
{
|
||||
'picture': '【解说】好的,各位,欢迎回到我的频道!《庆余年 2》刚开播就给了我们一个王炸!范闲在北齐"死"了?这怎么可能!',
|
||||
'timestamp': '00:00:00-00:01:15',
|
||||
'timestamp': '00:00:00,001-00:01:15,001',
|
||||
'narration': '好的各位,欢迎回到我的频道!《庆余年 2》刚开播就给了我们一个王炸!范闲在北齐"死"了?这怎么可能!上集片尾那个巨大的悬念,这一集就立刻揭晓了!范闲假死归来,他面临的第一个,也是最大的难关,就是如何面对他最敬爱的,同时也是最可怕的那个人——庆帝!',
|
||||
'OST': 0,
|
||||
'_id': 1
|
||||
},
|
||||
{
|
||||
'picture': '【解说】上一集我们看到,范闲在北齐遭遇了惊天变故,生死不明!',
|
||||
'timestamp': '00:01:15-00:04:40',
|
||||
'timestamp': '00:01:15,001-00:04:40,001',
|
||||
'narration': '但我们都知道,他绝不可能就这么轻易退场!第二集一开场,范闲就已经秘密回到了京都。他的生死传闻,可不像我们想象中那样只是小范围流传,而是…',
|
||||
'OST': 0,
|
||||
'_id': 2
|
||||
},
|
||||
{
|
||||
'picture': '画面切到王启年小心翼翼地向范闲汇报。',
|
||||
'timestamp': '00:04:41-00:04:58',
|
||||
'timestamp': '00:04:41,001-00:04:58,001',
|
||||
'narration': '我发现大人的死讯不光是在民间,在官场上也它传开了,所以呢,所以啊,可不是什么好事,将来您跟陛下怎么交代,这可是欺君之罪',
|
||||
'OST': 1,
|
||||
'_id': 3
|
||||
},
|
||||
{
|
||||
'picture': '【解说】"欺君之罪"!在封建王朝,这可是抄家灭族的大罪!搁一般人,肯定脚底抹油溜之大吉了。',
|
||||
'timestamp': '00:04:58-00:05:45',
|
||||
'timestamp': '00:04:58,001-00:05:45,001',
|
||||
'narration': '"欺君之罪"!在封建王朝,这可是抄家灭族的大罪!搁一般人,肯定脚底抹油溜之大吉了。但范闲是谁啊?他偏要反其道而行之!他竟然决定,直接去见庆帝!冒着天大的风险,用"假死"这个事实去赌庆帝的态度!',
|
||||
'OST': 0,
|
||||
'_id': 4
|
||||
},
|
||||
{
|
||||
'picture': '【解说】但想见庆帝,哪有那么容易?范闲艺高人胆大,竟然选择了最激进的方式——闯宫!',
|
||||
'timestamp': '00:05:45-00:06:00',
|
||||
'timestamp': '00:05:45,001-00:06:00,001',
|
||||
'narration': '但想见庆帝,哪有那么容易?范闲艺高人胆大,竟然选择了最激进的方式——闯宫!',
|
||||
'OST': 0,
|
||||
'_id': 5
|
||||
},
|
||||
{
|
||||
'picture': '画面切换到范闲蒙面闯入皇宫,被侍卫包围的场景。',
|
||||
'timestamp': '00:06:00-00:06:03',
|
||||
'timestamp': '00:06:00,001-00:06:03,001',
|
||||
'narration': '抓刺客',
|
||||
'OST': 1,
|
||||
'_id': 6
|
||||
}]
|
||||
video_res = {
|
||||
1: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/2c0c2ae91b4f58596634a0b2f64d3eb0/vid-00-00-00-00-00-26.mp4',
|
||||
2: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/2c0c2ae91b4f58596634a0b2f64d3eb0/vid-00-01-15-00-01-29.mp4',
|
||||
4: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/2c0c2ae91b4f58596634a0b2f64d3eb0/vid-00-04-58-00-05-20.mp4',
|
||||
5: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/2c0c2ae91b4f58596634a0b2f64d3eb0/vid-00-05-45-00-05-53.mp4'}
|
||||
1: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/fc3db5844d1ba7d7d838be52c0dac1bd/vid_00-00-00-000@00-00-20-250.mp4',
|
||||
2: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/fc3db5844d1ba7d7d838be52c0dac1bd/vid_00-00-30-000@00-00-48-950.mp4',
|
||||
4: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/fc3db5844d1ba7d7d838be52c0dac1bd/vid_00-01-00-000@00-01-15-688.mp4',
|
||||
5: '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/fc3db5844d1ba7d7d838be52c0dac1bd/vid_00-01-30-000@00-01-49-512.mp4'}
|
||||
audio_res = {
|
||||
1: '/Users/apple/Desktop/home/NarratoAI/storage/tasks/qyn2-2-demo/audio_00_00_00-00_01_15.mp3',
|
||||
2: '/Users/apple/Desktop/home/NarratoAI/storage/tasks/qyn2-2-demo/audio_00_01_15-00_04_40.mp3',
|
||||
|
||||
@ -6,6 +6,7 @@ streamlit~=1.45.0
|
||||
watchdog==6.0.0
|
||||
loguru~=0.7.3
|
||||
tomli~=2.2.1
|
||||
pydub==0.25.1
|
||||
|
||||
openai~=1.77.0
|
||||
google-generativeai>=0.8.5
|
||||
|
||||
134
webui.py
134
webui.py
@ -120,77 +120,75 @@ def tr(key):
|
||||
def render_generate_button():
|
||||
"""渲染生成按钮和处理逻辑"""
|
||||
if st.button(tr("Generate Video"), use_container_width=True, type="primary"):
|
||||
from app.services import task as tm
|
||||
|
||||
# 重置日志容器和记录
|
||||
log_container = st.empty()
|
||||
log_records = []
|
||||
|
||||
def log_received(msg):
|
||||
with log_container:
|
||||
log_records.append(msg)
|
||||
st.code("\n".join(log_records))
|
||||
|
||||
from loguru import logger
|
||||
logger.add(log_received)
|
||||
|
||||
config.save_config()
|
||||
task_id = st.session_state.get('task_id')
|
||||
|
||||
if not task_id:
|
||||
st.error(tr("请先裁剪视频"))
|
||||
return
|
||||
if not st.session_state.get('video_clip_json_path'):
|
||||
st.error(tr("脚本文件不能为空"))
|
||||
return
|
||||
if not st.session_state.get('video_origin_path'):
|
||||
st.error(tr("视频文件不能为空"))
|
||||
return
|
||||
|
||||
st.toast(tr("生成视频"))
|
||||
logger.info(tr("开始生成视频"))
|
||||
|
||||
# 获取所有参数
|
||||
script_params = script_settings.get_script_params()
|
||||
video_params = video_settings.get_video_params()
|
||||
audio_params = audio_settings.get_audio_params()
|
||||
subtitle_params = subtitle_settings.get_subtitle_params()
|
||||
|
||||
# 合并所有参数
|
||||
all_params = {
|
||||
**script_params,
|
||||
**video_params,
|
||||
**audio_params,
|
||||
**subtitle_params
|
||||
}
|
||||
|
||||
# 创建参数对象
|
||||
params = VideoClipParams(**all_params)
|
||||
|
||||
result = tm.start_subclip(
|
||||
task_id=task_id,
|
||||
params=params,
|
||||
subclip_path_videos=st.session_state['subclip_videos']
|
||||
)
|
||||
|
||||
video_files = result.get("videos", [])
|
||||
st.success(tr("视生成完成"))
|
||||
|
||||
try:
|
||||
from app.services import task as tm
|
||||
import torch
|
||||
if video_files:
|
||||
player_cols = st.columns(len(video_files) * 2 + 1)
|
||||
for i, url in enumerate(video_files):
|
||||
player_cols[i * 2 + 1].video(url)
|
||||
except Exception as e:
|
||||
logger.error(f"播放视频失败: {e}")
|
||||
|
||||
# 重置日志容器和记录
|
||||
log_container = st.empty()
|
||||
log_records = []
|
||||
file_utils.open_task_folder(config.root_dir, task_id)
|
||||
logger.info(tr("视频生成完成"))
|
||||
|
||||
def log_received(msg):
|
||||
with log_container:
|
||||
log_records.append(msg)
|
||||
st.code("\n".join(log_records))
|
||||
|
||||
from loguru import logger
|
||||
logger.add(log_received)
|
||||
|
||||
config.save_config()
|
||||
task_id = st.session_state.get('task_id')
|
||||
|
||||
if not task_id:
|
||||
st.error(tr("请先裁剪视频"))
|
||||
return
|
||||
if not st.session_state.get('video_clip_json_path'):
|
||||
st.error(tr("脚本文件不能为空"))
|
||||
return
|
||||
if not st.session_state.get('video_origin_path'):
|
||||
st.error(tr("视频文件不能为空"))
|
||||
return
|
||||
|
||||
st.toast(tr("生成视频"))
|
||||
logger.info(tr("开始生成视频"))
|
||||
|
||||
# 获取所有参数
|
||||
script_params = script_settings.get_script_params()
|
||||
video_params = video_settings.get_video_params()
|
||||
audio_params = audio_settings.get_audio_params()
|
||||
subtitle_params = subtitle_settings.get_subtitle_params()
|
||||
|
||||
# 合并所有参数
|
||||
all_params = {
|
||||
**script_params,
|
||||
**video_params,
|
||||
**audio_params,
|
||||
**subtitle_params
|
||||
}
|
||||
|
||||
# 创建参数对象
|
||||
params = VideoClipParams(**all_params)
|
||||
|
||||
result = tm.start_subclip(
|
||||
task_id=task_id,
|
||||
params=params,
|
||||
subclip_path_videos=st.session_state['subclip_videos']
|
||||
)
|
||||
|
||||
video_files = result.get("videos", [])
|
||||
st.success(tr("视生成完成"))
|
||||
|
||||
try:
|
||||
if video_files:
|
||||
player_cols = st.columns(len(video_files) * 2 + 1)
|
||||
for i, url in enumerate(video_files):
|
||||
player_cols[i * 2 + 1].video(url)
|
||||
except Exception as e:
|
||||
logger.error(f"播放视频失败: {e}")
|
||||
|
||||
file_utils.open_task_folder(config.root_dir, task_id)
|
||||
logger.info(tr("视频生成完成"))
|
||||
|
||||
finally:
|
||||
PerformanceMonitor.cleanup_resources()
|
||||
# finally:
|
||||
# PerformanceMonitor.cleanup_resources()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user