diff --git a/app/services/clip_video.py b/app/services/clip_video.py index 1c5fddf..8574173 100644 --- a/app/services/clip_video.py +++ b/app/services/clip_video.py @@ -546,6 +546,359 @@ def try_fallback_encoding( return execute_simple_command(fallback_cmd, timestamp, "通用Fallback") +def _process_narration_only_segment( + video_origin_path: str, + script_item: Dict, + tts_map: Dict, + output_dir: str, + encoder_config: Dict, + hwaccel_args: List[str] +) -> Optional[str]: + """ + 处理OST=0的纯解说片段 + - 根据TTS音频时长动态裁剪 + - 移除原声,生成静音视频 + """ + _id = script_item["_id"] + timestamp = script_item["timestamp"] + + # 获取对应的TTS结果 + tts_item = tts_map.get(_id) + if not tts_item: + logger.error(f"未找到片段 {_id} 的TTS结果") + return None + + # 解析起始时间,使用TTS音频时长计算结束时间 + start_time, _ = parse_timestamp(timestamp) + duration = tts_item["duration"] + calculated_end_time = calculate_end_time(start_time, duration, extra_seconds=0) + + # 转换为FFmpeg兼容的时间格式 + ffmpeg_start_time = start_time.replace(',', '.') + ffmpeg_end_time = calculated_end_time.replace(',', '.') + + # 生成输出文件名 + safe_start_time = start_time.replace(':', '-').replace(',', '-') + safe_end_time = calculated_end_time.replace(':', '-').replace(',', '-') + output_filename = f"ost0_vid_{safe_start_time}@{safe_end_time}.mp4" + output_path = os.path.join(output_dir, output_filename) + + # 构建FFmpeg命令 - 移除音频 + cmd = _build_ffmpeg_command_with_audio_control( + video_origin_path, output_path, ffmpeg_start_time, ffmpeg_end_time, + encoder_config, hwaccel_args, remove_audio=True + ) + + # 执行命令 + success = execute_ffmpeg_with_fallback( + cmd, timestamp, video_origin_path, output_path, + ffmpeg_start_time, ffmpeg_end_time + ) + + return output_path if success else None + + +def _process_original_audio_segment( + video_origin_path: str, + script_item: Dict, + output_dir: str, + encoder_config: Dict, + hwaccel_args: List[str] +) -> Optional[str]: + """ + 处理OST=1的纯原声片段 + - 严格按照脚本timestamp精确裁剪 + - 保持原声不变 + """ + _id = script_item["_id"] + timestamp = script_item["timestamp"] + + # 严格按照timestamp进行裁剪 + start_time, end_time = parse_timestamp(timestamp) + + # 转换为FFmpeg兼容的时间格式 + ffmpeg_start_time = start_time.replace(',', '.') + ffmpeg_end_time = end_time.replace(',', '.') + + # 生成输出文件名 + safe_start_time = start_time.replace(':', '-').replace(',', '-') + safe_end_time = end_time.replace(':', '-').replace(',', '-') + output_filename = f"ost1_vid_{safe_start_time}@{safe_end_time}.mp4" + output_path = os.path.join(output_dir, output_filename) + + # 构建FFmpeg命令 - 保持原声 + cmd = _build_ffmpeg_command_with_audio_control( + video_origin_path, output_path, ffmpeg_start_time, ffmpeg_end_time, + encoder_config, hwaccel_args, remove_audio=False + ) + + # 执行命令 + success = execute_ffmpeg_with_fallback( + cmd, timestamp, video_origin_path, output_path, + ffmpeg_start_time, ffmpeg_end_time + ) + + return output_path if success else None + + +def _process_mixed_segment( + video_origin_path: str, + script_item: Dict, + tts_map: Dict, + output_dir: str, + encoder_config: Dict, + hwaccel_args: List[str] +) -> Optional[str]: + """ + 处理OST=2的解说+原声混合片段 + - 根据TTS音频时长动态裁剪 + - 保持原声,确保视频时长等于TTS音频时长 + """ + _id = script_item["_id"] + timestamp = script_item["timestamp"] + + # 获取对应的TTS结果 + tts_item = tts_map.get(_id) + if not tts_item: + logger.error(f"未找到片段 {_id} 的TTS结果") + return None + + # 解析起始时间,使用TTS音频时长计算结束时间 + start_time, _ = parse_timestamp(timestamp) + duration = tts_item["duration"] + calculated_end_time = calculate_end_time(start_time, duration, extra_seconds=0) + + # 转换为FFmpeg兼容的时间格式 + ffmpeg_start_time = start_time.replace(',', '.') + ffmpeg_end_time = calculated_end_time.replace(',', '.') + + # 生成输出文件名 + safe_start_time = start_time.replace(':', '-').replace(',', '-') + safe_end_time = calculated_end_time.replace(':', '-').replace(',', '-') + output_filename = f"ost2_vid_{safe_start_time}@{safe_end_time}.mp4" + output_path = os.path.join(output_dir, output_filename) + + # 构建FFmpeg命令 - 保持原声 + cmd = _build_ffmpeg_command_with_audio_control( + video_origin_path, output_path, ffmpeg_start_time, ffmpeg_end_time, + encoder_config, hwaccel_args, remove_audio=False + ) + + # 执行命令 + success = execute_ffmpeg_with_fallback( + cmd, timestamp, video_origin_path, output_path, + ffmpeg_start_time, ffmpeg_end_time + ) + + return output_path if success else None + + +def _build_ffmpeg_command_with_audio_control( + input_path: str, + output_path: str, + start_time: str, + end_time: str, + encoder_config: Dict[str, str], + hwaccel_args: List[str] = None, + remove_audio: bool = False +) -> List[str]: + """ + 构建支持音频控制的FFmpeg命令 + + Args: + input_path: 输入视频路径 + output_path: 输出视频路径 + start_time: 开始时间 + end_time: 结束时间 + encoder_config: 编码器配置 + hwaccel_args: 硬件加速参数 + remove_audio: 是否移除音频(OST=0时为True) + + Returns: + List[str]: ffmpeg命令列表 + """ + cmd = ["ffmpeg", "-y"] + + # 硬件加速设置(参考原有逻辑) + if encoder_config["video_codec"] == "h264_nvenc": + # 对于NVENC,不使用硬件解码以避免滤镜链问题 + pass + elif hwaccel_args: + cmd.extend(hwaccel_args) + + # 输入文件 + cmd.extend(["-i", input_path]) + + # 时间范围 + cmd.extend(["-ss", start_time, "-to", end_time]) + + # 视频编码器设置 + cmd.extend(["-c:v", encoder_config["video_codec"]]) + + # 音频处理 + if remove_audio: + # OST=0: 移除音频 + cmd.extend(["-an"]) # -an 表示不包含音频流 + logger.debug("OST=0: 移除音频流") + else: + # OST=1,2: 保持原声 + cmd.extend(["-c:a", encoder_config["audio_codec"]]) + cmd.extend(["-ar", "44100", "-ac", "2"]) + logger.debug("OST=1/2: 保持原声") + + # 像素格式 + cmd.extend(["-pix_fmt", encoder_config["pixel_format"]]) + + # 质量和预设参数(参考原有逻辑) + if encoder_config["video_codec"] == "h264_nvenc": + cmd.extend(["-preset", encoder_config["preset"]]) + cmd.extend(["-cq", encoder_config["quality_value"]]) + cmd.extend(["-profile:v", "main"]) + elif encoder_config["video_codec"] == "h264_amf": + cmd.extend(["-quality", encoder_config["preset"]]) + cmd.extend(["-qp_i", encoder_config["quality_value"]]) + elif encoder_config["video_codec"] == "h264_qsv": + cmd.extend(["-preset", encoder_config["preset"]]) + cmd.extend(["-global_quality", encoder_config["quality_value"]]) + elif encoder_config["video_codec"] == "h264_videotoolbox": + cmd.extend(["-profile:v", "high"]) + cmd.extend(["-b:v", encoder_config["quality_value"]]) + else: + # 软件编码器(libx264) + cmd.extend(["-preset", encoder_config["preset"]]) + cmd.extend(["-crf", encoder_config["quality_value"]]) + + # 优化参数 + cmd.extend(["-avoid_negative_ts", "make_zero"]) + cmd.extend(["-movflags", "+faststart"]) + + # 输出文件 + cmd.append(output_path) + + return cmd + + +def clip_video_unified( + video_origin_path: str, + script_list: List[Dict], + tts_results: List[Dict], + output_dir: Optional[str] = None, + task_id: Optional[str] = None +) -> Dict[str, str]: + """ + 基于OST类型的统一视频裁剪策略 - 消除双重裁剪问题 + + Args: + video_origin_path: 原始视频的路径 + script_list: 完整的脚本列表,包含所有片段信息 + tts_results: TTS结果列表,仅包含OST=0和OST=2的片段 + output_dir: 输出目录路径,默认为None时会自动生成 + task_id: 任务ID,用于生成唯一的输出目录,默认为None时会自动生成 + + Returns: + Dict[str, str]: 片段ID到裁剪后视频路径的映射 + """ + # 检查视频文件是否存在 + if not os.path.exists(video_origin_path): + raise FileNotFoundError(f"视频文件不存在: {video_origin_path}") + + # 如果未提供task_id,则根据输入生成一个唯一ID + if task_id is None: + content_for_hash = f"{video_origin_path}_{json.dumps(script_list)}" + task_id = hashlib.md5(content_for_hash.encode()).hexdigest() + + # 设置输出目录 + if output_dir is None: + output_dir = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + "storage", "temp", "clip_video_unified", task_id + ) + + # 确保输出目录存在 + Path(output_dir).mkdir(parents=True, exist_ok=True) + + # 创建TTS结果的快速查找映射 + tts_map = {item['_id']: item for item in tts_results} + + # 获取硬件加速支持 + hwaccel_type = check_hardware_acceleration() + hwaccel_args = [] + + if hwaccel_type: + hwaccel_args = ffmpeg_utils.get_ffmpeg_hwaccel_args() + hwaccel_info = ffmpeg_utils.get_ffmpeg_hwaccel_info() + logger.info(f"🚀 使用硬件加速: {hwaccel_type} ({hwaccel_info.get('message', '')})") + else: + logger.info("🔧 使用软件编码") + + # 获取编码器配置 + encoder_config = get_safe_encoder_config(hwaccel_type) + logger.debug(f"编码器配置: {encoder_config}") + + # 统计信息 + total_clips = len(script_list) + result = {} + failed_clips = [] + success_count = 0 + + logger.info(f"📹 开始统一视频裁剪,总共{total_clips}个片段") + + for i, script_item in enumerate(script_list, 1): + _id = script_item.get("_id") + ost = script_item.get("OST", 0) + timestamp = script_item["timestamp"] + + logger.info(f"📹 [{i}/{total_clips}] 处理片段 ID:{_id}, OST:{ost}, 时间戳:{timestamp}") + + try: + if ost == 0: # 纯解说片段 + output_path = _process_narration_only_segment( + video_origin_path, script_item, tts_map, output_dir, + encoder_config, hwaccel_args + ) + elif ost == 1: # 纯原声片段 + output_path = _process_original_audio_segment( + video_origin_path, script_item, output_dir, + encoder_config, hwaccel_args + ) + elif ost == 2: # 解说+原声混合片段 + output_path = _process_mixed_segment( + video_origin_path, script_item, tts_map, output_dir, + encoder_config, hwaccel_args + ) + else: + logger.warning(f"未知的OST类型: {ost},跳过片段 {_id}") + continue + + if output_path and os.path.exists(output_path) and os.path.getsize(output_path) > 0: + result[_id] = output_path + success_count += 1 + logger.info(f"✅ [{i}/{total_clips}] 片段处理成功: OST={ost}, ID={_id}") + else: + failed_clips.append(f"ID:{_id}, OST:{ost}") + logger.error(f"❌ [{i}/{total_clips}] 片段处理失败: OST={ost}, ID={_id}") + + except Exception as e: + failed_clips.append(f"ID:{_id}, OST:{ost}") + logger.error(f"❌ [{i}/{total_clips}] 片段处理异常: OST={ost}, ID={_id}, 错误: {str(e)}") + + # 最终统计 + logger.info(f"📊 统一视频裁剪完成: 成功 {success_count}/{total_clips}, 失败 {len(failed_clips)}") + + # 检查是否有失败的片段 + if failed_clips: + logger.warning(f"⚠️ 以下片段处理失败: {failed_clips}") + if len(failed_clips) == total_clips: + raise RuntimeError("所有视频片段处理都失败了,请检查视频文件和ffmpeg配置") + elif len(failed_clips) > total_clips / 2: + logger.warning(f"⚠️ 超过一半的片段处理失败 ({len(failed_clips)}/{total_clips}),请检查硬件加速配置") + + if success_count > 0: + logger.info(f"🎉 统一视频裁剪任务完成! 输出目录: {output_dir}") + + return result + + def clip_video( video_origin_path: str, tts_result: List[Dict], diff --git a/app/services/task.py b/app/services/task.py index 3a81584..6150247 100644 --- a/app/services/task.py +++ b/app/services/task.py @@ -15,13 +15,19 @@ from app.services import state as sm from app.utils import utils -def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: dict): +def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: dict = None): """ - 后台任务(自动剪辑视频进行剪辑) + 后台任务(统一视频裁剪处理)- 优化版本 + + 实施基于OST类型的统一视频裁剪策略,消除双重裁剪问题: + - OST=0: 根据TTS音频时长动态裁剪,移除原声 + - OST=1: 严格按照脚本timestamp精确裁剪,保持原声 + - OST=2: 根据TTS音频时长动态裁剪,保持原声 + Args: task_id: 任务ID params: 视频参数 - subclip_path_videos: 视频片段路径 + subclip_path_videos: 视频片段路径(可选,仅作为备用方案) """ global merged_audio_path, merged_subtitle_path @@ -94,17 +100,26 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: di # sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=40) """ - 3. 裁剪视频 - 将超出音频长度的视频进行裁剪 + 3. 统一视频裁剪 - 基于OST类型的差异化裁剪策略 """ - logger.info("\n\n## 3. 裁剪视频") - video_clip_result = clip_video.clip_video(params.video_origin_path, tts_results) - # 更新 list_script 中的时间戳 + logger.info("\n\n## 3. 统一视频裁剪(基于OST类型)") + + # 使用新的统一裁剪策略 + video_clip_result = clip_video.clip_video_unified( + video_origin_path=params.video_origin_path, + script_list=list_script, + tts_results=tts_results + ) + + # 更新 list_script 中的时间戳和路径信息 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 } new_script_list = update_script.update_script_timestamps(list_script, video_clip_result, tts_clip_result, subclip_clip_result) + logger.info(f"统一裁剪完成,处理了 {len(video_clip_result)} 个视频片段") + sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=60) """ @@ -139,8 +154,27 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: di combined_video_path = path.join(utils.task_dir(task_id), f"merger.mp4") logger.info(f"\n\n## 5. 合并视频: => {combined_video_path}") - # 如果 new_script_list 中没有 video,则使用 subclip_path_videos 中的视频 - video_clips = [new_script['video'] if new_script.get('video') else subclip_path_videos.get(new_script.get('_id', '')) for new_script in new_script_list] + + # 使用统一裁剪后的视频片段 + video_clips = [] + for new_script in new_script_list: + video_path = new_script.get('video') + if video_path and os.path.exists(video_path): + video_clips.append(video_path) + else: + logger.warning(f"片段 {new_script.get('_id')} 的视频文件不存在或未生成: {video_path}") + # 如果统一裁剪失败,尝试使用备用方案(如果提供了subclip_path_videos) + if subclip_path_videos and new_script.get('_id') in subclip_path_videos: + backup_video = subclip_path_videos[new_script.get('_id')] + if os.path.exists(backup_video): + video_clips.append(backup_video) + logger.info(f"使用备用视频: {backup_video}") + else: + logger.error(f"备用视频也不存在: {backup_video}") + else: + logger.error(f"无法找到片段 {new_script.get('_id')} 的视频文件") + + logger.info(f"准备合并 {len(video_clips)} 个视频片段") merger_video.combine_clip_videos( output_video_path=combined_video_path, @@ -208,6 +242,199 @@ def start_subclip(task_id: str, params: VideoClipParams, subclip_path_videos: di return kwargs +def start_subclip_unified(task_id: str, params: VideoClipParams): + """ + 统一视频裁剪处理函数 - 完全基于OST类型的新实现 + + 这是优化后的版本,完全移除了对预裁剪视频的依赖, + 实现真正的统一裁剪策略。 + + Args: + task_id: 任务ID + params: 视频参数 + """ + global merged_audio_path, merged_subtitle_path + + logger.info(f"\n\n## 开始统一视频处理任务: {task_id}") + sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=0) + + """ + 1. 加载剪辑脚本 + """ + logger.info("\n\n## 1. 加载视频脚本") + video_script_path = path.join(params.video_clip_json_path) + + if path.exists(video_script_path): + try: + with open(video_script_path, "r", encoding="utf-8") as f: + list_script = json.load(f) + video_list = [i['narration'] for i in list_script] + video_ost = [i['OST'] for i in list_script] + time_list = [i['timestamp'] for i in list_script] + + video_script = " ".join(video_list) + logger.debug(f"解说完整脚本: \n{video_script}") + logger.debug(f"解说 OST 列表: \n{video_ost}") + logger.debug(f"解说时间戳列表: \n{time_list}") + except Exception as e: + logger.error(f"无法读取视频json脚本,请检查脚本格式是否正确") + raise ValueError("无法读取视频json脚本,请检查脚本格式是否正确") + else: + logger.error(f"video_script_path: {video_script_path}") + raise ValueError("解说脚本不存在!请检查配置是否正确。") + + """ + 2. 使用 TTS 生成音频素材 + """ + logger.info("\n\n## 2. 根据OST设置生成音频列表") + # 只为OST=0 or 2的判断生成音频, OST=0 仅保留解说 OST=2 保留解说和原声 + tts_segments = [ + segment for segment in list_script + if segment['OST'] in [0, 2] + ] + logger.debug(f"需要生成TTS的片段数: {len(tts_segments)}") + + tts_results = voice.tts_multiple( + task_id=task_id, + list_script=tts_segments, # 只传入需要TTS的片段 + voice_name=params.voice_name, + voice_rate=params.voice_rate, + voice_pitch=params.voice_pitch, + ) + + sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=20) + + """ + 3. 统一视频裁剪 - 基于OST类型的差异化裁剪策略 + """ + logger.info("\n\n## 3. 统一视频裁剪(基于OST类型)") + + # 使用新的统一裁剪策略 + video_clip_result = clip_video.clip_video_unified( + video_origin_path=params.video_origin_path, + script_list=list_script, + tts_results=tts_results + ) + + # 更新 list_script 中的时间戳和路径信息 + 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 + } + new_script_list = update_script.update_script_timestamps(list_script, video_clip_result, tts_clip_result, subclip_clip_result) + + logger.info(f"统一裁剪完成,处理了 {len(video_clip_result)} 个视频片段") + + sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=60) + + """ + 4. 合并音频和字幕 + """ + logger.info("\n\n## 4. 合并音频和字幕") + total_duration = sum([script["duration"] for script in new_script_list]) + if tts_segments: + try: + # 合并音频文件 + merged_audio_path = audio_merger.merge_audio_files( + task_id=task_id, + total_duration=total_duration, + list_script=new_script_list + ) + logger.info(f"音频文件合并成功->{merged_audio_path}") + # 合并字幕文件 + merged_subtitle_path = subtitle_merger.merge_subtitle_files(new_script_list) + logger.info(f"字幕文件合并成功->{merged_subtitle_path}") + except Exception as e: + logger.error(f"合并音频文件失败: {str(e)}") + else: + logger.warning("没有需要合并的音频/字幕") + merged_audio_path = "" + merged_subtitle_path = "" + + """ + 5. 合并视频 + """ + final_video_paths = [] + combined_video_paths = [] + + combined_video_path = path.join(utils.task_dir(task_id), f"merger.mp4") + logger.info(f"\n\n## 5. 合并视频: => {combined_video_path}") + + # 使用统一裁剪后的视频片段 + video_clips = [] + for new_script in new_script_list: + video_path = new_script.get('video') + if video_path and os.path.exists(video_path): + video_clips.append(video_path) + else: + logger.error(f"片段 {new_script.get('_id')} 的视频文件不存在: {video_path}") + + logger.info(f"准备合并 {len(video_clips)} 个视频片段") + + merger_video.combine_clip_videos( + output_video_path=combined_video_path, + video_paths=video_clips, + video_ost_list=video_ost, + video_aspect=params.video_aspect, + threads=params.n_threads + ) + sm.state.update_task(task_id, state=const.TASK_STATE_PROCESSING, progress=80) + + """ + 6. 合并字幕/BGM/配音/视频 + """ + output_video_path = path.join(utils.task_dir(task_id), f"combined.mp4") + logger.info(f"\n\n## 6. 最后一步: 合并字幕/BGM/配音/视频 -> {output_video_path}") + + bgm_path = utils.get_bgm_file() + + # 获取优化的音量配置 + optimized_volumes = get_recommended_volumes_for_content('mixed') + + # 应用用户设置和优化建议的组合 + final_tts_volume = params.tts_volume if hasattr(params, 'tts_volume') and params.tts_volume != 1.0 else optimized_volumes['tts_volume'] + final_original_volume = params.original_volume if hasattr(params, 'original_volume') and params.original_volume != 0.7 else optimized_volumes['original_volume'] + final_bgm_volume = params.bgm_volume if hasattr(params, 'bgm_volume') and params.bgm_volume != 0.3 else optimized_volumes['bgm_volume'] + + logger.info(f"音量配置 - TTS: {final_tts_volume}, 原声: {final_original_volume}, BGM: {final_bgm_volume}") + + # 调用示例 + options = { + 'voice_volume': final_tts_volume, + 'bgm_volume': final_bgm_volume, + 'original_audio_volume': final_original_volume, + 'keep_original_audio': True, + 'subtitle_enabled': params.subtitle_enabled, + 'subtitle_font': params.font_name, + 'subtitle_font_size': params.font_size, + 'subtitle_color': params.text_fore_color, + 'subtitle_bg_color': None, + 'subtitle_position': params.subtitle_position, + 'custom_position': params.custom_position, + 'threads': params.n_threads + } + generate_video.merge_materials( + video_path=combined_video_path, + audio_path=merged_audio_path, + subtitle_path=merged_subtitle_path, + bgm_path=bgm_path, + output_path=output_video_path, + options=options + ) + + final_video_paths.append(output_video_path) + combined_video_paths.append(combined_video_path) + + logger.success(f"统一处理任务 {task_id} 已完成, 生成 {len(final_video_paths)} 个视频.") + + kwargs = { + "videos": final_video_paths, + "combined_videos": combined_video_paths + } + sm.state.update_task(task_id, state=const.TASK_STATE_COMPLETE, progress=100, **kwargs) + return kwargs + + def validate_params(video_path, audio_path, output_file, params): """ 验证输入参数 diff --git a/app/utils/utils.py b/app/utils/utils.py index 1dbf7e3..d101dce 100644 --- a/app/utils/utils.py +++ b/app/utils/utils.py @@ -509,6 +509,12 @@ def clean_model_output(output): def cut_video(params, progress_callback=None): + """ + 旧的视频裁剪函数 - 已弃用 + + 注意:此函数已被统一裁剪策略取代,不再推荐使用。 + 新的实现请使用 task.start_subclip_unified() 函数。 + """ try: task_id = str(uuid4()) st.session_state['task_id'] = task_id diff --git a/webui.py b/webui.py index 9d82838..56e2c39 100644 --- a/webui.py +++ b/webui.py @@ -106,8 +106,7 @@ def init_global_state(): st.session_state['video_plot'] = '' if 'ui_language' not in st.session_state: st.session_state['ui_language'] = config.ui.get("language", utils.get_system_locale()) - if 'subclip_videos' not in st.session_state: - st.session_state['subclip_videos'] = {} + # 移除subclip_videos初始化 - 现在使用统一裁剪策略 def tr(key): @@ -136,11 +135,9 @@ def render_generate_button(): logger.add(log_received) config.save_config() - task_id = st.session_state.get('task_id') - if not task_id: - st.error(tr("请先裁剪视频")) - return + # 移除task_id检查 - 现在使用统一裁剪策略,不再需要预裁剪 + # 直接检查必要的文件是否存在 if not st.session_state.get('video_clip_json_path'): st.error(tr("脚本文件不能为空")) return @@ -168,10 +165,14 @@ def render_generate_button(): # 创建参数对象 params = VideoClipParams(**all_params) - result = tm.start_subclip( + # 使用新的统一裁剪策略,不再需要预裁剪的subclip_videos + # 生成一个新的task_id用于本次处理 + import uuid + task_id = str(uuid.uuid4()) + + result = tm.start_subclip_unified( task_id=task_id, - params=params, - subclip_path_videos=st.session_state['subclip_videos'] + params=params ) video_files = result.get("videos", []) diff --git a/webui/components/script_settings.py b/webui/components/script_settings.py index b452d08..0caa122 100644 --- a/webui/components/script_settings.py +++ b/webui/components/script_settings.py @@ -336,8 +336,8 @@ def render_script_buttons(tr, params): height=180 ) - # 操作按钮行 - button_cols = st.columns(3) + # 操作按钮行 - 移除裁剪视频按钮,使用统一裁剪策略 + button_cols = st.columns(2) # 改为2列布局 with button_cols[0]: if st.button(tr("Check Format"), key="check_format", use_container_width=True): check_script_format(tr, video_clip_json_details) @@ -346,11 +346,6 @@ def render_script_buttons(tr, params): if st.button(tr("Save Script"), key="save_script", use_container_width=True): save_script(tr, video_clip_json_details) - with button_cols[2]: - script_valid = st.session_state.get('script_format_valid', False) - if st.button(tr("Crop Video"), key="crop_video", disabled=not script_valid, use_container_width=True): - crop_video(tr, params) - def check_script_format(tr, script_content): """检查脚本格式""" @@ -414,26 +409,7 @@ def save_script(tr, video_clip_json_details): st.stop() -def crop_video(tr, params): - """裁剪视频""" - progress_bar = st.progress(0) - status_text = st.empty() - - def update_progress(progress): - progress_bar.progress(progress) - status_text.text(f"剪辑进度: {progress}%") - - try: - utils.cut_video(params, update_progress) - time.sleep(0.5) - progress_bar.progress(100) - st.success("视频剪辑成功完成!") - except Exception as e: - st.error(f"剪辑过程中发生错误: {str(e)}") - finally: - time.sleep(1) - progress_bar.empty() - status_text.empty() +# crop_video函数已移除 - 现在使用统一裁剪策略,不再需要预裁剪步骤 def get_script_params(): diff --git a/webui/i18n/zh.json b/webui/i18n/zh.json index aad77e8..61c0e11 100644 --- a/webui/i18n/zh.json +++ b/webui/i18n/zh.json @@ -11,7 +11,6 @@ "Video Theme": "视频主题", "Generation Prompt": "自定义提示词", "Save Script": "保存脚本", - "Crop Video": "裁剪视频", "Video File": "视频文件(:blue[1️⃣支持上传视频文件(限制2G) 2️⃣大文件建议直接导入 ./resource/videos 目录])", "Plot Description": "剧情描述 (:blue[可从 https://www.tvmao.com/ 获取])", "Generate Video Keywords": "点击使用AI根据**文案**生成【视频关键】",