diff --git a/app/services/audio_merger.py b/app/services/audio_merger.py index 510c6b7..bedb585 100644 --- a/app/services/audio_merger.py +++ b/app/services/audio_merger.py @@ -69,7 +69,7 @@ def merge_audio_files(task_id: str, total_duration: float, list_script: list): continue # 保存合并后的音频文件 - output_audio_path = os.path.join(utils.task_dir(task_id), "final_audio.mp3") + output_audio_path = os.path.join(utils.task_dir(task_id), "merger_audio.mp3") final_audio.export(output_audio_path, format="mp3") logger.info(f"合并后的音频文件已保存: {output_audio_path}") diff --git a/app/services/clip_video.py b/app/services/clip_video.py index 73b7456..c10faf4 100644 --- a/app/services/clip_video.py +++ b/app/services/clip_video.py @@ -144,6 +144,7 @@ def clip_video( result = {} for item in tts_result: + _id = item["_id"] timestamp = item["timestamp"] start_time, _ = parse_timestamp(timestamp) @@ -180,7 +181,7 @@ def clip_video( check=True ) - result[timestamp] = output_path + result[_id] = output_path except subprocess.CalledProcessError as e: logger.error(f"裁剪视频片段失败: {timestamp}") diff --git a/app/services/task.py b/app/services/task.py index 42853f2..640d821 100644 --- a/app/services/task.py +++ b/app/services/task.py @@ -217,7 +217,6 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: di voice_name=params.voice_name, voice_rate=params.voice_rate, voice_pitch=params.voice_pitch, - force_regenerate=True ) sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=20) @@ -245,12 +244,13 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: di 3. 裁剪视频 - 将超出音频长度的视频进行裁剪 """ logger.info("\n\n## 3. 裁剪视频") - clip_result = clip_video.clip_video(params.video_origin_path, tts_results) - subclip_path_videos.update(clip_result) + video_clip_result = clip_video.clip_video(params.video_origin_path, tts_results) # 更新 list_script 中的时间戳 - tts_clip_result = {tts_result['timestamp']: tts_result['audio_file'] for tts_result in tts_results} - list_script = update_script.update_script_timestamps(list_script, clip_result, tts_clip_result) - subclip_videos = [x for x in subclip_path_videos.values()] + tts_clip_result = {tts_result['_id']: tts_result['audio_file'] for tts_result in tts_results} + subclip_clip_result = { + tts_result['_id']: tts_result['subtitle_file'] for tts_result in tts_results + } + list_script = update_script.update_script_timestamps(list_script, video_clip_result, tts_clip_result, subclip_clip_result) sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=60) @@ -268,14 +268,14 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: di merged_audio_path = audio_merger.merge_audio_files( task_id=task_id, total_duration=total_duration, - list_script=list_script # 传入完整脚本以便处理OST + list_script=list_script ) logger.info(f"音频文件合并成功->{merged_audio_path}") - # 合并字幕文件 - merged_subtitle_path = subtitle_merger.merge_subtitle_files( - subtitle_files=subtitle_files, - ) - logger.info(f"字幕文件合并成功->{merged_subtitle_path}") + # # 合并字幕文件 + # merged_subtitle_path = subtitle_merger.merge_subtitle_files( + # subtitle_files=subtitle_files, + # ) + # logger.info(f"字幕文件合并成功->{merged_subtitle_path}") except Exception as e: logger.error(f"合并音频文件失败: {str(e)}") merged_audio_path = "" @@ -285,7 +285,7 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: di return """ - 6. 合并视频 + 5. 合并视频 """ final_video_paths = [] combined_video_paths = [] diff --git a/app/services/update_script.py b/app/services/update_script.py index 3213b6d..7083a23 100644 --- a/app/services/update_script.py +++ b/app/services/update_script.py @@ -10,7 +10,7 @@ import re import os -from typing import Dict, List, Any, Tuple +from typing import Dict, List, Any, Tuple, Union def extract_timestamp_from_video_path(video_path: str) -> str: @@ -63,14 +63,21 @@ def calculate_duration(timestamp: str) -> float: return 0.0 -def update_script_timestamps(script_list: List[Dict[str, Any]], video_result: Dict[str, str], audio_result: Dict[str, str] = None) -> List[Dict[str, Any]]: +def update_script_timestamps( + script_list: List[Dict[str, Any]], + video_result: Dict[Union[str, int], str], + audio_result: Dict[Union[str, int], str] = None, + subtitle_result: Dict[Union[str, int], str] = None +) -> List[Dict[str, Any]]: """ - 根据 video_result 中的视频文件更新 script_list 中的时间戳,添加持续时间,并根据 audio_result 添加音频路径 + 根据 video_result 中的视频文件更新 script_list 中的时间戳,添加持续时间, + 并根据 audio_result 添加音频路径,根据 subtitle_result 添加字幕路径 Args: script_list: 原始脚本列表 - video_result: 视频结果字典,键为原时间戳,值为视频文件路径 - audio_result: 音频结果字典,键为原时间戳,值为音频文件路径 + video_result: 视频结果字典,键为原时间戳或_id,值为视频文件路径 + audio_result: 音频结果字典,键为原时间戳或_id,值为音频文件路径 + subtitle_result: 字幕结果字典,键为原时间戳或_id,值为字幕文件路径 Returns: 更新后的脚本列表 @@ -78,12 +85,12 @@ def update_script_timestamps(script_list: List[Dict[str, Any]], video_result: Di # 创建副本,避免修改原始数据 updated_script = [] - # 建立原始时间戳到视频路径和新时间戳的映射 - timestamp_mapping = {} - for orig_timestamp, video_path in video_result.items(): + # 建立ID和时间戳到视频路径和新时间戳的映射 + id_timestamp_mapping = {} + for key, video_path in video_result.items(): new_timestamp = extract_timestamp_from_video_path(video_path) if new_timestamp: - timestamp_mapping[orig_timestamp] = { + id_timestamp_mapping[key] = { 'new_timestamp': new_timestamp, 'video_path': video_path } @@ -91,19 +98,35 @@ def update_script_timestamps(script_list: List[Dict[str, Any]], video_result: Di # 更新脚本中的时间戳 for item in script_list: item_copy = item.copy() + item_id = item_copy.get('_id') orig_timestamp = item_copy.get('timestamp', '') - # 初始化音频路径为空字符串 + # 初始化音频和字幕路径为空字符串 item_copy['audio'] = "" + item_copy['subtitle'] = "" - # 如果提供了音频结果字典且时间戳存在于音频结果中,直接使用对应的音频路径 - if audio_result and orig_timestamp in audio_result: - item_copy['audio'] = audio_result[orig_timestamp] + # 如果提供了音频结果字典且ID存在于音频结果中,直接使用对应的音频路径 + if audio_result: + if item_id and item_id in audio_result: + item_copy['audio'] = audio_result[item_id] + elif orig_timestamp in audio_result: + item_copy['audio'] = audio_result[orig_timestamp] - if orig_timestamp in timestamp_mapping: - # 更新时间戳 - item_copy['timestamp'] = timestamp_mapping[orig_timestamp]['new_timestamp'] - # 计算持续时间 + # 如果提供了字幕结果字典且ID存在于字幕结果中,直接使用对应的字幕路径 + if subtitle_result: + if item_id and item_id in subtitle_result: + item_copy['subtitle'] = subtitle_result[item_id] + elif orig_timestamp in subtitle_result: + item_copy['subtitle'] = subtitle_result[orig_timestamp] + + # 更新时间戳和计算持续时间 + if item_id and item_id in id_timestamp_mapping: + # 根据ID找到对应的新时间戳 + item_copy['timestamp'] = id_timestamp_mapping[item_id]['new_timestamp'] + item_copy['duration'] = calculate_duration(item_copy['timestamp']) + elif orig_timestamp in id_timestamp_mapping: + # 根据原始时间戳找到对应的新时间戳 + item_copy['timestamp'] = id_timestamp_mapping[orig_timestamp]['new_timestamp'] item_copy['duration'] = calculate_duration(item_copy['timestamp']) elif orig_timestamp: # 对于未更新的时间戳,也计算并添加持续时间 @@ -116,36 +139,66 @@ def update_script_timestamps(script_list: List[Dict[str, Any]], video_result: Di if __name__ == '__main__': list_script = [ - {'picture': '【解说】好的,各位,欢迎回到我的频道!《庆余年 2》刚开播就给了我们一个王炸!范闲在北齐"死"了?这怎么可能!', - 'timestamp': '00:00:00-00:01:15', - 'narration': '好的各位,欢迎回到我的频道!《庆余年 2》刚开播就给了我们一个王炸!范闲在北齐"死"了?这怎么可能!上集片尾那个巨大的悬念,这一集就立刻揭晓了!范闲假死归来,他面临的第一个,也是最大的难关,就是如何面对他最敬爱的,同时也是最可怕的那个人——庆帝!', - 'OST': 0}, - {'picture': '【解说】上一集我们看到,范闲在北齐遭遇了惊天变故,生死不明!', 'timestamp': '00:01:15-00:04:40', - 'narration': '但我们都知道,他绝不可能就这么轻易退场!第二集一开场,范闲就已经秘密回到了京都。他的生死传闻,可不像我们想象中那样只是小范围流传,而是…', - 'OST': 0}, {'picture': '画面切到王启年小心翼翼地向范闲汇报。', 'timestamp': '00:04:41-00:04:58', - 'narration': '我发现大人的死讯不光是在民间,在官场上也它传开了,所以呢,所以啊,可不是什么好事,将来您跟陛下怎么交代,这可是欺君之罪', - 'OST': 1}, - {'picture': '【解说】"欺君之罪"!在封建王朝,这可是抄家灭族的大罪!搁一般人,肯定脚底抹油溜之大吉了。', - 'timestamp': '00:04:58-00:05:45', - 'narration': '"欺君之罪"!在封建王朝,这可是抄家灭族的大罪!搁一般人,肯定脚底抹油溜之大吉了。但范闲是谁啊?他偏要反其道而行之!他竟然决定,直接去见庆帝!冒着天大的风险,用"假死"这个事实去赌庆帝的态度!', - 'OST': 0}, {'picture': '【解说】但想见庆帝,哪有那么容易?范闲艺高人胆大,竟然选择了最激进的方式——闯宫!', - 'timestamp': '00:05:45-00:06:00', - 'narration': '但想见庆帝,哪有那么容易?范闲艺高人胆大,竟然选择了最激进的方式——闯宫!', 'OST': 0}, - {'picture': '画面切换到范闲蒙面闯入皇宫,被侍卫包围的场景。', 'timestamp': '00:06:00-00:06:03', - 'narration': '抓刺客', 'OST': 1}] + { + 'picture': '【解说】好的,各位,欢迎回到我的频道!《庆余年 2》刚开播就给了我们一个王炸!范闲在北齐"死"了?这怎么可能!', + 'timestamp': '00:00:00-00:01:15', + 'narration': '好的各位,欢迎回到我的频道!《庆余年 2》刚开播就给了我们一个王炸!范闲在北齐"死"了?这怎么可能!上集片尾那个巨大的悬念,这一集就立刻揭晓了!范闲假死归来,他面临的第一个,也是最大的难关,就是如何面对他最敬爱的,同时也是最可怕的那个人——庆帝!', + 'OST': 0, + '_id': 1 + }, + { + 'picture': '【解说】上一集我们看到,范闲在北齐遭遇了惊天变故,生死不明!', + 'timestamp': '00:01:15-00:04:40', + 'narration': '但我们都知道,他绝不可能就这么轻易退场!第二集一开场,范闲就已经秘密回到了京都。他的生死传闻,可不像我们想象中那样只是小范围流传,而是…', + 'OST': 0, + '_id': 2 + }, + { + 'picture': '画面切到王启年小心翼翼地向范闲汇报。', + 'timestamp': '00:04:41-00:04:58', + 'narration': '我发现大人的死讯不光是在民间,在官场上也它传开了,所以呢,所以啊,可不是什么好事,将来您跟陛下怎么交代,这可是欺君之罪', + 'OST': 1, + '_id': 3 + }, + { + 'picture': '【解说】"欺君之罪"!在封建王朝,这可是抄家灭族的大罪!搁一般人,肯定脚底抹油溜之大吉了。', + 'timestamp': '00:04:58-00:05:45', + 'narration': '"欺君之罪"!在封建王朝,这可是抄家灭族的大罪!搁一般人,肯定脚底抹油溜之大吉了。但范闲是谁啊?他偏要反其道而行之!他竟然决定,直接去见庆帝!冒着天大的风险,用"假死"这个事实去赌庆帝的态度!', + 'OST': 0, + '_id': 4 + }, + { + 'picture': '【解说】但想见庆帝,哪有那么容易?范闲艺高人胆大,竟然选择了最激进的方式——闯宫!', + 'timestamp': '00:05:45-00:06:00', + 'narration': '但想见庆帝,哪有那么容易?范闲艺高人胆大,竟然选择了最激进的方式——闯宫!', + 'OST': 0, + '_id': 5 + }, + { + 'picture': '画面切换到范闲蒙面闯入皇宫,被侍卫包围的场景。', + 'timestamp': '00:06:00-00:06:03', + 'narration': '抓刺客', + 'OST': 1, + '_id': 6 + }] video_res = { - '00:00:00-00:01:15': '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/0ac14d474144b54d614c26a5c87cffe7/vid-00-00-00-00-00-26.mp4', - '00:01:15-00:04:40': '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/0ac14d474144b54d614c26a5c87cffe7/vid-00-01-15-00-01-29.mp4', - '00:04:58-00:05:45': '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/0ac14d474144b54d614c26a5c87cffe7/vid-00-04-58-00-05-20.mp4', - '00:05:45-00:06:00': '/Users/apple/Desktop/home/NarratoAI/storage/temp/clip_video/0ac14d474144b54d614c26a5c87cffe7/vid-00-05-45-00-05-53.mp4'} + 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'} audio_res = { - '00:00:00-00:01:15': '/Users/apple/Desktop/home/NarratoAI/storage/tasks/qyn2-2-demo/audio_00_00_00-00_01_15.mp3', - '00:01:15-00:04:40': '/Users/apple/Desktop/home/NarratoAI/storage/tasks/qyn2-2-demo/audio_00_01_15-00_04_40.mp3', - '00:04:58-00:05:45': '/Users/apple/Desktop/home/NarratoAI/storage/tasks/qyn2-2-demo/audio_00_04_58-00_05_45.mp3', - '00:05:45-00:06:00': '/Users/apple/Desktop/home/NarratoAI/storage/tasks/qyn2-2-demo/audio_00_05_45-00_06_00.mp3'} + 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', + 4: '/Users/apple/Desktop/home/NarratoAI/storage/tasks/qyn2-2-demo/audio_00_04_58-00_05_45.mp3', + 5: '/Users/apple/Desktop/home/NarratoAI/storage/tasks/qyn2-2-demo/audio_00_05_45-00_06_00.mp3'} + sub_res = { + 1: '/Users/apple/Desktop/home/NarratoAI/storage/tasks/qyn2-2-demo/subtitle_00_00_00-00_01_15.srt', + 2: '/Users/apple/Desktop/home/NarratoAI/storage/tasks/qyn2-2-demo/subtitle_00_01_15-00_04_40.srt', + 4: '/Users/apple/Desktop/home/NarratoAI/storage/tasks/qyn2-2-demo/subtitle_00_04_58-00_05_45.srt', + 5: '/Users/apple/Desktop/home/NarratoAI/storage/tasks/qyn2-2-demo/subtitle_00_05_45-00_06_00.srt'} # 更新并打印结果 - updated_list_script = update_script_timestamps(list_script, video_res, audio_res) + updated_list_script = update_script_timestamps(list_script, video_res, audio_res, sub_res) for item in updated_list_script: print( - f"Picture: {item['picture'][:20]}... | Timestamp: {item['timestamp']} | Duration: {item['duration']} 秒 | Audio: {item['audio']}") + f"ID: {item['_id']} | Picture: {item['picture'][:20]}... | Timestamp: {item['timestamp']} | Duration: {item['duration']} 秒 | Audio: {item['audio']} | Subtitle: {item['subtitle']}") diff --git a/app/services/voice.py b/app/services/voice.py index f5570e4..48197a8 100644 --- a/app/services/voice.py +++ b/app/services/voice.py @@ -1411,7 +1411,7 @@ def get_audio_duration(sub_maker: submaker.SubMaker): return sub_maker.offset[-1][1] / 10000000 -def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: float, voice_pitch: float, force_regenerate: bool = True): +def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: float, voice_pitch: float): """ 根据JSON文件中的多段文本进行TTS转换 @@ -1419,7 +1419,6 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f :param list_script: 脚本列表 :param voice_name: 语音名称 :param voice_rate: 语音速率 - :param force_regenerate: 是否强制重新生成已存在的音频文件 :return: 生成的音频文件列表 """ voice_name = parse_voice_name(voice_name) @@ -1427,22 +1426,11 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f tts_results = [] for item in list_script: - tts_item = { - "audio_file": "", - "subtitle_file": "", - "duration": 0, - } if item['OST'] != 1: # 将时间戳中的冒号替换为下划线 timestamp = item['timestamp'].replace(':', '_') audio_file = os.path.join(output_dir, f"audio_{timestamp}.mp3") subtitle_file = os.path.join(output_dir, f"subtitle_{timestamp}.srt") - - # # 检查文件是否已存在,如存在且不强制重新生成,则跳过 - # if os.path.exists(audio_file) and not force_regenerate: - # logger.info(f"音频文件已存在,跳过生成: {audio_file}") - # tts_item["audio_file"] = audio_file - # continue text = item['narration'] @@ -1464,6 +1452,7 @@ def tts_multiple(task_id: str, list_script: list, voice_name: str, voice_rate: f _, duration = create_subtitle(sub_maker=sub_maker, text=text, subtitle_file=subtitle_file) tts_results.append({ + "_id": item['_id'], "timestamp": item['timestamp'], "audio_file": audio_file, "subtitle_file": subtitle_file,