mirror of
https://github.com/linyqh/NarratoAI.git
synced 2026-01-24 04:18:14 +00:00
优化剪辑逻辑123
This commit is contained in:
parent
67d6f353eb
commit
e440dc619f
108
app/services/audio_merger.py
Normal file
108
app/services/audio_merger.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import edge_tts
|
||||||
|
from edge_tts import submaker
|
||||||
|
from pydub import AudioSegment
|
||||||
|
from typing import List
|
||||||
|
from loguru import logger
|
||||||
|
from app.utils import utils
|
||||||
|
|
||||||
|
|
||||||
|
def check_ffmpeg():
|
||||||
|
"""检查FFmpeg是否已安装"""
|
||||||
|
try:
|
||||||
|
subprocess.run(['ffmpeg', '-version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
return True
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def merge_audio_files(task_id: str, audio_file_paths: List[str], total_duration: int):
|
||||||
|
"""
|
||||||
|
合并多个音频文件到一个指定总时长的音频文件中
|
||||||
|
|
||||||
|
:param audio_file_paths: 音频文件路径列表
|
||||||
|
:param total_duration: 最终音频文件的总时长(秒)
|
||||||
|
"""
|
||||||
|
output_dir = utils.task_dir(task_id)
|
||||||
|
|
||||||
|
if not check_ffmpeg():
|
||||||
|
logger.error("错误:FFmpeg未安装。请安装FFmpeg后再运行此脚本。")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 创建一个总时长为total_duration的空白音频
|
||||||
|
blank_audio = AudioSegment.silent(duration=total_duration * 1000) # pydub使用毫秒
|
||||||
|
# 创建SubMaker对象
|
||||||
|
sub_maker = edge_tts.SubMaker()
|
||||||
|
|
||||||
|
for audio_path in audio_file_paths:
|
||||||
|
if not os.path.exists(audio_path):
|
||||||
|
logger.info(f"警告:文件 {audio_path} 不存在,已跳过。")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 从文件名中提取时间戳
|
||||||
|
filename = os.path.basename(audio_path)
|
||||||
|
start_time, end_time = extract_timestamp(filename)
|
||||||
|
|
||||||
|
# 读取音频文件
|
||||||
|
try:
|
||||||
|
audio = AudioSegment.from_mp3(audio_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"错误:无法读取文件 {audio_path}。错误信息:{str(e)}")
|
||||||
|
continue
|
||||||
|
# 将音频插入到空白音频的指定位置
|
||||||
|
blank_audio = blank_audio.overlay(audio, position=start_time * 1000)
|
||||||
|
|
||||||
|
# 添加字幕信息
|
||||||
|
duration = (end_time - start_time) * 1000 # 转换为毫秒
|
||||||
|
# TODO 不是 filename 需要考虑怎么把字幕文本弄过来
|
||||||
|
sub_maker.create_sub((start_time * 1000, duration), filename)
|
||||||
|
|
||||||
|
# 尝试导出为WAV格式
|
||||||
|
try:
|
||||||
|
output_file = os.path.join(output_dir, "audio.wav")
|
||||||
|
blank_audio.export(output_file, format="wav")
|
||||||
|
logger.info(f"音频合并完成,已保存为 {output_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f"导出为WAV格式失败,尝试使用MP3格式:{str(e)}")
|
||||||
|
try:
|
||||||
|
output_file = "merged_audio.mp3"
|
||||||
|
blank_audio.export(output_file, format="mp3", codec="libmp3lame")
|
||||||
|
logger.info(f"音频合并完成,已保存为 {output_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"导出音频失败:{str(e)}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
return output_file, sub_maker
|
||||||
|
|
||||||
|
|
||||||
|
def extract_timestamp(filename):
|
||||||
|
"""从文件名中提取开始和结束时间戳"""
|
||||||
|
time_part = filename.split('_')[1].split('.')[0]
|
||||||
|
times = time_part.split('-')
|
||||||
|
|
||||||
|
# 将时间戳转换为秒
|
||||||
|
start_seconds = time_to_seconds(times[0], times[1])
|
||||||
|
end_seconds = time_to_seconds(times[2], times[3])
|
||||||
|
|
||||||
|
return start_seconds, end_seconds
|
||||||
|
|
||||||
|
|
||||||
|
def time_to_seconds(minutes, seconds):
|
||||||
|
"""将分钟和秒转换为总秒数"""
|
||||||
|
return int(minutes) * 60 + int(seconds)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 示例用法
|
||||||
|
audio_files = [
|
||||||
|
"/Users/apple/Desktop/home/NarratoAI/storage/tasks/test456/audio_00-06-00-24.mp3",
|
||||||
|
"/Users/apple/Desktop/home/NarratoAI/storage/tasks/test456/audio_00-32-00-38.mp3",
|
||||||
|
"/Users/apple/Desktop/home/NarratoAI/storage/tasks/test456/audio_00-43-00-52.mp3",
|
||||||
|
"/Users/apple/Desktop/home/NarratoAI/storage/tasks/test456/audio_00-52-01-09.mp3",
|
||||||
|
"/Users/apple/Desktop/home/NarratoAI/storage/tasks/test456/audio_01-13-01-15.mp3"
|
||||||
|
]
|
||||||
|
total_duration = 75
|
||||||
|
|
||||||
|
a, b = merge_audio_files("test456", audio_files, total_duration)
|
||||||
|
print(a, b)
|
||||||
@ -817,8 +817,8 @@ if __name__ == "__main__":
|
|||||||
# gemini_video_transcription(video_subject, video_path, language)
|
# gemini_video_transcription(video_subject, video_path, language)
|
||||||
|
|
||||||
# 2. 解说文案
|
# 2. 解说文案
|
||||||
# video_path = "/Users/apple/Desktop/home/NarratoAI/resource/videos/1.mp4"
|
video_path = "/Users/apple/Desktop/home/NarratoAI/resource/videos/1.mp4"
|
||||||
video_path = "E:\\projects\\NarratoAI\\resource\\videos\\1.mp4"
|
# video_path = "E:\\projects\\NarratoAI\\resource\\videos\\1.mp4"
|
||||||
video_plot = """
|
video_plot = """
|
||||||
李自忠拿着儿子李牧名下的存折,去银行取钱给儿子救命,却被要求证明"你儿子是你儿子"。
|
李自忠拿着儿子李牧名下的存折,去银行取钱给儿子救命,却被要求证明"你儿子是你儿子"。
|
||||||
走投无路时碰到银行被抢劫,劫匪给了他两沓钱救命,李自忠却因此被银行以抢劫罪起诉,并顶格判处20年有期徒刑。
|
走投无路时碰到银行被抢劫,劫匪给了他两沓钱救命,李自忠却因此被银行以抢劫罪起诉,并顶格判处20年有期徒刑。
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from loguru import logger
|
|||||||
from app.config import config
|
from app.config import config
|
||||||
from app.models import const
|
from app.models import const
|
||||||
from app.models.schema import VideoConcatMode, VideoParams, VideoClipParams
|
from app.models.schema import VideoConcatMode, VideoParams, VideoClipParams
|
||||||
from app.services import llm, material, subtitle, video, voice
|
from app.services import llm, material, subtitle, video, voice, audio_merger
|
||||||
from app.services import state as sm
|
from app.services import state as sm
|
||||||
from app.utils import utils
|
from app.utils import utils
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ def generate_subtitle(task_id, params, video_script, sub_maker, audio_file):
|
|||||||
if not params.subtitle_enabled:
|
if not params.subtitle_enabled:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
subtitle_path = path.join(utils.task_dir(task_id), "subtitle.srt")
|
subtitle_path = path.join(utils.task_dir(task_id), "subtitle111.srt")
|
||||||
subtitle_provider = config.app.get("subtitle_provider", "").strip().lower()
|
subtitle_provider = config.app.get("subtitle_provider", "").strip().lower()
|
||||||
logger.info(f"\n\n## generating subtitle, provider: {subtitle_provider}")
|
logger.info(f"\n\n## generating subtitle, provider: {subtitle_provider}")
|
||||||
|
|
||||||
@ -353,6 +353,9 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos):
|
|||||||
logger.debug(f"解说完整脚本: \n{video_script}")
|
logger.debug(f"解说完整脚本: \n{video_script}")
|
||||||
logger.debug(f"解说 OST 列表: \n{video_ost}")
|
logger.debug(f"解说 OST 列表: \n{video_ost}")
|
||||||
logger.debug(f"解说时间戳列表: \n{time_list}")
|
logger.debug(f"解说时间戳列表: \n{time_list}")
|
||||||
|
# 获取视频总时长(单位 s)
|
||||||
|
total_duration = list_script[-1]['new_timestamp']
|
||||||
|
total_duration = int(total_duration.split("-")[1].split(":")[0]) * 60 + int(total_duration.split("-")[1].split(":")[1])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"无法读取视频json脚本,请检查配置是否正确。{e}")
|
logger.error(f"无法读取视频json脚本,请检查配置是否正确。{e}")
|
||||||
raise ValueError("无法读取视频json脚本,请检查配置是否正确")
|
raise ValueError("无法读取视频json脚本,请检查配置是否正确")
|
||||||
@ -372,21 +375,27 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos):
|
|||||||
logger.error(
|
logger.error(
|
||||||
"音频文件为空,可能是网络不可用。如果您在中国,请使用VPN。或者手动选择 zh-CN-Yunjian-男性 音频")
|
"音频文件为空,可能是网络不可用。如果您在中国,请使用VPN。或者手动选择 zh-CN-Yunjian-男性 音频")
|
||||||
return
|
return
|
||||||
|
logger.info("合并音频")
|
||||||
|
audio_file, sub_maker = audio_merger.merge_audio_files(task_id, audio_files, total_duration)
|
||||||
|
|
||||||
|
# audio_duration = voice.get_audio_duration(sub_maker)
|
||||||
|
# audio_duration = math.ceil(audio_duration)
|
||||||
sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=30)
|
sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=30)
|
||||||
|
|
||||||
subtitle_path = ""
|
subtitle_path = ""
|
||||||
if params.subtitle_enabled:
|
if params.subtitle_enabled:
|
||||||
subtitle_path = path.join(utils.task_dir(task_id), f"subtitle.srt")
|
subtitle_path = path.join(utils.task_dir(task_id), f"subtitle111.srt")
|
||||||
subtitle_provider = config.app.get("subtitle_provider", "").strip().lower()
|
subtitle_provider = config.app.get("subtitle_provider", "").strip().lower()
|
||||||
logger.info(f"\n\n## 3. 生成字幕、提供程序是: {subtitle_provider}")
|
logger.info(f"\n\n## 3. 生成字幕、提供程序是: {subtitle_provider}")
|
||||||
# subtitle_fallback = False
|
# subtitle_fallback = False
|
||||||
if subtitle_provider == "edge":
|
if subtitle_provider == "edge":
|
||||||
voice.create_subtitle_from_multiple(
|
voice.create_subtitle(text=video_script, sub_maker=sub_maker, subtitle_file=subtitle_path)
|
||||||
text=video_script,
|
# voice.create_subtitle(
|
||||||
sub_maker_list=sub_maker_list,
|
# text=video_script,
|
||||||
list_script=list_script,
|
# sub_maker_list=sub_maker_list,
|
||||||
subtitle_file=subtitle_path
|
# list_script=list_script,
|
||||||
)
|
# subtitle_file=subtitle_path
|
||||||
|
# )
|
||||||
# if not os.path.exists(subtitle_path):
|
# if not os.path.exists(subtitle_path):
|
||||||
# subtitle_fallback = True
|
# subtitle_fallback = True
|
||||||
# logger.warning("找不到字幕文件,回退到whisper")
|
# logger.warning("找不到字幕文件,回退到whisper")
|
||||||
@ -475,18 +484,35 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
task_id = "test123456"
|
# task_id = "test123"
|
||||||
subclip_path_videos = {'00:41-01:58': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_41-01_58.mp4',
|
# subclip_path_videos = {'00:41-01:58': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_41-01_58.mp4',
|
||||||
'00:06-00:15': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_06-00_15.mp4',
|
# '00:06-00:15': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_06-00_15.mp4',
|
||||||
'01:10-01:17': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-01_10-01_17.mp4',
|
# '01:10-01:17': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-01_10-01_17.mp4',
|
||||||
'00:47-01:03': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_47-01_03.mp4',
|
# '00:47-01:03': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-00_47-01_03.mp4',
|
||||||
'01:03-01:10': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-01_03-01_10.mp4',
|
# '01:03-01:10': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-01_03-01_10.mp4',
|
||||||
'02:40-03:08': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-02_40-03_08.mp4',
|
# '02:40-03:08': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-02_40-03_08.mp4',
|
||||||
'03:02-03:20': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-03_02-03_20.mp4',
|
# '03:02-03:20': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-03_02-03_20.mp4',
|
||||||
'03:18-03:20': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-03_18-03_20.mp4'}
|
# '03:18-03:20': 'E:\\projects\\NarratoAI\\storage\\cache_videos/vid-03_18-03_20.mp4'}
|
||||||
|
#
|
||||||
|
# params = VideoClipParams(
|
||||||
|
# video_clip_json_path="E:\\projects\\NarratoAI\\resource/scripts/test003.json",
|
||||||
|
# video_origin_path="E:\\projects\\NarratoAI\\resource/videos/1.mp4",
|
||||||
|
# )
|
||||||
|
# start_subclip(task_id, params, subclip_path_videos=subclip_path_videos)
|
||||||
|
|
||||||
|
task_id = "test456"
|
||||||
|
subclip_path_videos = {'00:00-00:06': './storage/cache_videos/vid-00_00-00_06.mp4',
|
||||||
|
'00:06-00:24': './storage/cache_videos/vid-00_06-00_24.mp4',
|
||||||
|
'01:28-01:36': './storage/cache_videos/vid-01_28-01_36.mp4',
|
||||||
|
'00:41-00:47': './storage/cache_videos/vid-00_41-00_47.mp4',
|
||||||
|
'01:58-02:03': './storage/cache_videos/vid-01_58-02_03.mp4',
|
||||||
|
'02:03-02:12': './storage/cache_videos/vid-02_03-02_12.mp4',
|
||||||
|
'02:40-02:57': './storage/cache_videos/vid-02_40-02_57.mp4',
|
||||||
|
'03:14-03:18': './storage/cache_videos/vid-03_14-03_18.mp4',
|
||||||
|
'03:18-03:20': './storage/cache_videos/vid-03_18-03_20.mp4'}
|
||||||
|
|
||||||
params = VideoClipParams(
|
params = VideoClipParams(
|
||||||
video_clip_json_path="E:\\projects\\NarratoAI\\resource/scripts/test003.json",
|
video_clip_json_path="/Users/apple/Desktop/home/NarratoAI/resource/scripts/test003.json",
|
||||||
video_origin_path="E:\\projects\\NarratoAI\\resource/videos/1.mp4",
|
video_origin_path="/Users/apple/Desktop/home/NarratoAI/resource/videos/1.mp4",
|
||||||
)
|
)
|
||||||
start_subclip(task_id, params, subclip_path_videos=subclip_path_videos)
|
start_subclip(task_id, params, subclip_path_videos=subclip_path_videos)
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import os
|
|||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import edge_tts
|
import edge_tts
|
||||||
import asyncio
|
import asyncio
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from typing import List
|
from typing import List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from edge_tts.submaker import mktimestamp
|
||||||
from xml.sax.saxutils import unescape
|
from xml.sax.saxutils import unescape
|
||||||
from edge_tts import submaker, SubMaker
|
from edge_tts import submaker, SubMaker
|
||||||
from moviepy.video.tools import subtitles
|
from moviepy.video.tools import subtitles
|
||||||
@ -1293,6 +1293,96 @@ def create_subtitle_from_multiple(text: str, sub_maker_list: List[SubMaker], lis
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
def create_subtitle(sub_maker: submaker.SubMaker, text: str, subtitle_file: str):
|
||||||
|
"""
|
||||||
|
优化字幕文件
|
||||||
|
1. 将字幕文件按照标点符号分割成多行
|
||||||
|
2. 逐行匹配字幕文件中的文本
|
||||||
|
3. 生成新的字幕文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
text = _format_text(text)
|
||||||
|
|
||||||
|
def formatter(idx: int, start_time: float, end_time: float, sub_text: str) -> str:
|
||||||
|
"""
|
||||||
|
1
|
||||||
|
00:00:00,000 --> 00:00:02,360
|
||||||
|
跑步是一项简单易行的运动
|
||||||
|
"""
|
||||||
|
start_t = mktimestamp(start_time).replace(".", ",")
|
||||||
|
end_t = mktimestamp(end_time).replace(".", ",")
|
||||||
|
return f"{idx}\n" f"{start_t} --> {end_t}\n" f"{sub_text}\n"
|
||||||
|
|
||||||
|
start_time = -1.0
|
||||||
|
sub_items = []
|
||||||
|
sub_index = 0
|
||||||
|
|
||||||
|
script_lines = utils.split_string_by_punctuations(text)
|
||||||
|
|
||||||
|
def match_line(_sub_line: str, _sub_index: int):
|
||||||
|
if len(script_lines) <= _sub_index:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
_line = script_lines[_sub_index]
|
||||||
|
if _sub_line == _line:
|
||||||
|
return script_lines[_sub_index].strip()
|
||||||
|
|
||||||
|
_sub_line_ = re.sub(r"[^\w\s]", "", _sub_line)
|
||||||
|
_line_ = re.sub(r"[^\w\s]", "", _line)
|
||||||
|
if _sub_line_ == _line_:
|
||||||
|
return _line_.strip()
|
||||||
|
|
||||||
|
_sub_line_ = re.sub(r"\W+", "", _sub_line)
|
||||||
|
_line_ = re.sub(r"\W+", "", _line)
|
||||||
|
if _sub_line_ == _line_:
|
||||||
|
return _line.strip()
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
sub_line = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
for _, (offset, sub) in enumerate(zip(sub_maker.offset, sub_maker.subs)):
|
||||||
|
_start_time, end_time = offset
|
||||||
|
if start_time < 0:
|
||||||
|
start_time = _start_time
|
||||||
|
|
||||||
|
sub = unescape(sub)
|
||||||
|
sub_line += sub
|
||||||
|
sub_text = match_line(sub_line, sub_index)
|
||||||
|
if sub_text:
|
||||||
|
sub_index += 1
|
||||||
|
line = formatter(
|
||||||
|
idx=sub_index,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
sub_text=sub_text,
|
||||||
|
)
|
||||||
|
sub_items.append(line)
|
||||||
|
start_time = -1.0
|
||||||
|
sub_line = ""
|
||||||
|
|
||||||
|
if len(sub_items) == len(script_lines):
|
||||||
|
with open(subtitle_file, "w", encoding="utf-8") as file:
|
||||||
|
file.write("\n".join(sub_items) + "\n")
|
||||||
|
try:
|
||||||
|
sbs = subtitles.file_to_subtitles(subtitle_file, encoding="utf-8")
|
||||||
|
duration = max([tb for ((ta, tb), txt) in sbs])
|
||||||
|
logger.info(
|
||||||
|
f"completed, subtitle file created: {subtitle_file}, duration: {duration}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"failed, error: {str(e)}")
|
||||||
|
os.remove(subtitle_file)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"failed, sub_items len: {len(sub_items)}, script_lines len: {len(script_lines)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"failed, error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def get_audio_duration(sub_maker: submaker.SubMaker):
|
def get_audio_duration(sub_maker: submaker.SubMaker):
|
||||||
"""
|
"""
|
||||||
获取音频时长
|
获取音频时长
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user