import os import gradio as gr import tempfile import random import subprocess import shutil import zipfile from datetime import datetime import json import concurrent.futures import time import platform # 储存目录设置 STORAGE_DIR = os.path.expanduser("~/video_storage") STORAGE_CONFIG = os.path.join(STORAGE_DIR, "storage_config.json") # 检测系统硬件加速支持 def detect_hardware_acceleration(): """检测可用的硬件加速""" hardware_accel = { 'nvidia': False, 'amd': False, 'intel': False, 'encoder': 'libx264' # 默认使用软件编码 } try: # 检查NVIDIA GPU result = subprocess.run(['ffmpeg', '-encoders'], capture_output=True, text=True) if 'h264_nvenc' in result.stdout: hardware_accel['nvidia'] = True hardware_accel['encoder'] = 'h264_nvenc' print("✅ 检测到NVIDIA GPU硬件加速支持") # 检查AMD GPU if 'h264_amf' in result.stdout: hardware_accel['amd'] = True if hardware_accel['encoder'] == 'libx264': hardware_accel['encoder'] = 'h264_amf' print("✅ 检测到AMD GPU硬件加速支持") # 检查Intel Quick Sync if 'h264_qsv' in result.stdout: hardware_accel['intel'] = True if hardware_accel['encoder'] == 'libx264': hardware_accel['encoder'] = 'h264_qsv' print("✅ 检测到Intel Quick Sync硬件加速支持") except Exception as e: print(f"⚠️ 硬件加速检测失败: {e}") return hardware_accel # 全局硬件加速信息 HARDWARE_ACCEL = detect_hardware_acceleration() def init_storage(): """初始化储存空间""" os.makedirs(STORAGE_DIR, exist_ok=True) if not os.path.exists(STORAGE_CONFIG): config = { "created_time": datetime.now().isoformat(), "total_videos": 0, "total_size_mb": 0 } with open(STORAGE_CONFIG, 'w', encoding='utf-8') as f: json.dump(config, f, ensure_ascii=False, indent=2) def save_to_storage(file_path, metadata=None): """保存文件到储存空间 [优化:移除即时更新配置]""" try: base_name = os.path.basename(file_path) target_path = os.path.join(STORAGE_DIR, base_name) # 避免重名覆盖 count = 1 name, ext = os.path.splitext(base_name) while os.path.exists(target_path): target_path = os.path.join(STORAGE_DIR, f"{name}_{count}{ext}") count += 1 shutil.copy2(file_path, target_path) return target_path except Exception as e: print(f"❌ 储存文件失败: {e}") return None def update_storage_config(): """更新储存配置信息""" try: video_files = [f for f in os.listdir(STORAGE_DIR) if f.lower().endswith(('.mp4', '.mov', '.avi', '.mkv'))] total_size = sum(os.path.getsize(os.path.join(STORAGE_DIR, f)) for f in video_files) / (1024 * 1024) config = { "updated_time": datetime.now().isoformat(), "total_videos": len(video_files), "total_size_mb": round(total_size, 1) } with open(STORAGE_CONFIG, 'w', encoding='utf-8') as f: json.dump(config, f, ensure_ascii=False, indent=2) except: pass def get_storage_info(): """获取储存空间信息""" try: if os.path.exists(STORAGE_CONFIG): with open(STORAGE_CONFIG, 'r', encoding='utf-8') as f: config = json.load(f) else: config = {"total_videos": 0, "total_size_mb": 0} video_files = [] if os.path.exists(STORAGE_DIR): for f in os.listdir(STORAGE_DIR): if f.lower().endswith(('.mp4', '.mov', '.avi', '.mkv')): file_path = os.path.join(STORAGE_DIR, f) size_mb = os.path.getsize(file_path) / (1024 * 1024) mod_time = datetime.fromtimestamp(os.path.getmtime(file_path)) video_files.append({ "name": f, "size_mb": round(size_mb, 1), "modified": mod_time.strftime('%Y-%m-%d %H:%M') }) video_files.sort(key=lambda x: x["modified"], reverse=True) return config, video_files except: return {"total_videos": 0, "total_size_mb": 0}, [] def download_all_storage(): """一键下载储存空间所有视频""" try: config, video_files = get_storage_info() if not video_files: return None, "⚠️ 储存空间为空,没有可下载的文件" # 创建打包文件 package_dir = tempfile.mkdtemp() timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') zip_path = os.path.join(package_dir, f"储存空间全部视频_{timestamp}.zip") with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: for video in video_files: video_path = os.path.join(STORAGE_DIR, video['name']) if os.path.exists(video_path): zipf.write(video_path, video['name']) # 添加清单 manifest = f"""# 储存空间视频清单 ## 📊 下载信息 - 下载时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - 视频总数: {len(video_files)} 个 - 总大小: {config['total_size_mb']}MB ## 📁 文件列表 """ for video in video_files: manifest += f"- {video['name']} ({video['size_mb']}MB) - {video['modified']}\n" manifest += """ ## 💡 使用说明 这些是您的储存空间中保存的所有混剪视频,可直接使用或进一步编辑。 --- FFmpeg 储存管理系统 """ zipf.writestr("视频清单.txt", manifest.encode('utf-8')) download_msg = f"✅ 已打包 {len(video_files)} 个视频文件,总大小 {config['total_size_mb']}MB" return zip_path, download_msg except Exception as e: return None, f"❌ 打包下载失败: {str(e)}" def download_selected_storage(selected_files): """下载选中的储存文件""" try: if not selected_files: return None, "⚠️ 请至少选择一个文件进行下载" # 创建打包文件 package_dir = tempfile.mkdtemp() timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') zip_path = os.path.join(package_dir, f"选中视频_{timestamp}.zip") total_size = 0 with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: valid_files = [] for filename in selected_files: video_path = os.path.join(STORAGE_DIR, filename) if os.path.exists(video_path): zipf.write(video_path, filename) size_mb = os.path.getsize(video_path) / (1024 * 1024) total_size += size_mb valid_files.append({"name": filename, "size_mb": round(size_mb, 1)}) if not valid_files: return None, "❌ 选中的文件都不存在" # 添加清单 manifest = f"""# 选中视频下载清单 ## 📊 下载信息 - 下载时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - 选中文件: {len(valid_files)} 个 - 总大小: {round(total_size, 1)}MB ## 📁 文件列表 """ for video in valid_files: manifest += f"- {video['name']} ({video['size_mb']}MB)\n" zipf.writestr("下载清单.txt", manifest.encode('utf-8')) download_msg = f"✅ 已打包 {len(valid_files)} 个选中文件,总大小 {round(total_size, 1)}MB" return zip_path, download_msg except Exception as e: return None, f"❌ 选择下载失败: {str(e)}" def delete_storage_file(filename): """从储存空间删除文件""" try: file_path = os.path.join(STORAGE_DIR, filename) if os.path.exists(file_path): os.remove(file_path) update_storage_config() return f"✅ 已删除文件: {filename}" else: return f"❌ 文件不存在: {filename}" except Exception as e: return f"❌ 删除失败: {str(e)}" def clear_storage(): """清空储存空间""" try: count = 0 if os.path.exists(STORAGE_DIR): for f in os.listdir(STORAGE_DIR): if f.lower().endswith(('.mp4', '.mov', '.avi', '.mkv')): os.remove(os.path.join(STORAGE_DIR, f)) count += 1 update_storage_config() return f"✅ 已清空储存空间,删除了 {count} 个文件" except Exception as e: return f"❌ 清空失败: {str(e)}" def get_optimal_threads(): """获取最优线程数""" try: cpu_count = os.cpu_count() if cpu_count: # 对于视频处理,使用CPU核心数的1.5倍通常效果较好 return max(1, min(cpu_count * 2, 16)) # 最多16个线程 return 4 except: return 4 def build_ffmpeg_command(input_path, start_time, duration, output_path, operation='cut'): """构建优化的FFmpeg命令""" threads = get_optimal_threads() encoder = HARDWARE_ACCEL['encoder'] # 处理中文文件名 - 使用绝对路径并确保路径正确 input_path = os.path.abspath(input_path) output_path = os.path.abspath(output_path) base_command = ['ffmpeg'] # 硬件加速解码 - 简化处理,避免兼容性问题 # if HARDWARE_ACCEL['nvidia']: # base_command.extend(['-hwaccel', 'cuda']) # elif HARDWARE_ACCEL['amd']: # base_command.extend(['-hwaccel', 'amf']) # elif HARDWARE_ACCEL['intel']: # base_command.extend(['-hwaccel', 'qsv']) base_command.extend(['-i', input_path]) if operation == 'cut': base_command.extend(['-ss', str(start_time), '-t', str(duration)]) # 编码参数 if encoder == 'h264_nvenc': # NVIDIA GPU编码参数 - 简化版本 base_command.extend([ '-c:v', 'h264_nvenc', '-preset', 'fast', # 使用fast而不是p1,更兼容 '-cq', '28', # 质量参数 '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', '-y', output_path ]) elif encoder == 'h264_amf': # AMD GPU编码参数 - 简化版本 base_command.extend([ '-c:v', 'h264_amf', '-quality', 'speed', '-rc', 'cqp', '-cqp_i', '28', '-cqp_p', '28', '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', '-y', output_path ]) elif encoder == 'h264_qsv': # Intel Quick Sync编码参数 - 简化版本 base_command.extend([ '-c:v', 'h264_qsv', '-preset', 'veryfast', '-global_quality', '28', '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', '-y', output_path ]) else: # 软件编码参数 - 使用ultrafast预设 base_command.extend([ '-c:v', 'libx264', '-preset', 'ultrafast', # 最快预设 '-crf', '28', # 质量参数 '-tune', 'fastdecode', # 优化解码速度 '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', '-avoid_negative_ts', 'make_zero', '-y', output_path ]) elif operation == 'resize': # 调整尺寸的编码参数 if encoder == 'h264_nvenc': base_command.extend([ '-c:v', 'h264_nvenc', '-preset', 'fast', '-cq', '28', '-c:a', 'copy', '-movflags', '+faststart', '-y', output_path ]) else: base_command.extend([ '-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '28', '-tune', 'fastdecode', '-c:a', 'copy', '-movflags', '+faststart', '-y', output_path ]) return base_command def ffmpeg_cut_video(input_path, start_time, duration, output_path): """[优化] 快速的视频切割,使用硬件加速和ultrafast预设""" try: command = build_ffmpeg_command(input_path, start_time, duration, output_path, 'cut') print(f"执行FFmpeg命令: {' '.join(command)}") # 调试输出 # 创建输出目录(如果不存在) os.makedirs(os.path.dirname(output_path), exist_ok=True) result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if result.returncode != 0: print(f"FFmpeg错误: {result.stderr}") # 输出错误信息 return False return os.path.exists(output_path) except Exception as e: print(f"切割视频异常: {e}") return False def ffmpeg_resize_video(input_path, output_path, target_ratio): """[优化] 快速的比例调整,使用硬件加速和ultrafast预设""" try: if target_ratio == '9:16': filter_complex = "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:black" else: filter_complex = "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:black" command = build_ffmpeg_command(input_path, 0, 0, output_path, 'resize') # 在resize操作中插入滤镜 command.insert(-5, '-vf') # 在-y之前插入 command.insert(-4, filter_complex) print(f"执行FFmpeg调整命令: {' '.join(command)}") # 调试输出 # 创建输出目录(如果不存在) os.makedirs(os.path.dirname(output_path), exist_ok=True) result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if result.returncode != 0: print(f"FFmpeg调整错误: {result.stderr}") # 输出错误信息 return False return os.path.exists(output_path) except Exception as e: print(f"调整视频异常: {e}") return False def concat_videos(file_list, output_path): """稳定的视频合并""" if not file_list: return False valid_files = [f for f in file_list if os.path.exists(f)] if not valid_files: return False list_file = tempfile.NamedTemporaryFile(delete=False, mode='w', suffix='.txt', encoding='utf-8') try: for f in valid_files: abs_path = os.path.abspath(f) list_file.write(f"file '{abs_path}'\n") list_file.close() # 使用多线程进行合并 threads = get_optimal_threads() command = ['ffmpeg', '-threads', str(threads), '-f', 'concat', '-safe', '0', '-i', list_file.name, '-c', 'copy', '-y', output_path] print(f"执行FFmpeg合并命令: {' '.join(command)}") # 调试输出 # 创建输出目录(如果不存在) os.makedirs(os.path.dirname(output_path), exist_ok=True) result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if result.returncode != 0: print(f"FFmpeg合并错误: {result.stderr}") # 输出错误信息 return False return os.path.exists(output_path) finally: try: os.unlink(list_file.name) except: pass def process_single_video(video_file, clip_duration, temp_dir): """[优化] 处理单个视频文件,返回其所有切片路径的列表""" video_path = video_file.name clips = [] try: # 获取视频总时长 cmd = ['ffprobe', '-v', 'quiet', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', video_path] result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) total_duration = float(result.stdout.strip()) print(f"视频总时长: {total_duration} 秒") except Exception as e: print(f"处理视频 {video_path} 时获取时长失败: {e}") return clips start = 0.0 count = 0 while start < total_duration: duration = min(clip_duration, total_duration - start) # 修改文件名,包含原始文件名和计数,避免不同视频的切片重名 clip_path = os.path.join(temp_dir, f"clip_{os.path.splitext(os.path.basename(video_path))[0]}_{count}.mp4") print(f"正在切割片段 {count}: {start}-{start+duration} 秒 -> {clip_path}") if ffmpeg_cut_video(video_path, start, duration, clip_path): clips.append(clip_path) print(f"✅ 切片成功: {clip_path}") else: print(f"❌ 切片失败: {clip_path}") start += clip_duration count += 1 return clips def process_videos_with_storage(video_files, clip_duration, num_output_videos, target_ratio): """带储存功能的视频处理 [优化版:硬件加速 + 并行切割 + 批量更新 + 进度日志]""" if not video_files: return "❌ 请上传视频文件", None, "", "" start_time = time.time() temp_dir = tempfile.mkdtemp() try: print(f"🔍 开始处理 {len(video_files)} 个视频文件...") print(f"⚡ 硬件加速: {HARDWARE_ACCEL['encoder']}") print(f"🔧 线程数: {get_optimal_threads()}") print(f"⏱️ 切片时长: {clip_duration} 秒 | 生成数量: {num_output_videos} 个 | 比例: {target_ratio}") all_clips = [] # 使用线程池进行并行处理(避免GIL限制) print(f"🚀 启动并行切片({min(4, os.cpu_count() or 1)} 线程)...") with concurrent.futures.ThreadPoolExecutor(max_workers=min(4, os.cpu_count() or 1)) as executor: future_to_video = { executor.submit(process_single_video, vf, clip_duration, temp_dir): vf for vf in video_files } completed = 0 for future in concurrent.futures.as_completed(future_to_video): video_file = future_to_video[future] try: video_clips = future.result() all_clips.extend(video_clips) completed += 1 print(f"✅ 已完成切片: {completed}/{len(video_files)} | 文件: {os.path.basename(video_file.name)} | 切片数: {len(video_clips)}") except Exception as exc: video_file = future_to_video[future] print(f"❌ 切片失败: {os.path.basename(video_file.name)} - {exc}") print(f"📊 所有视频切片完成,共生成 {len(all_clips)} 个片段") if not all_clips: return "❌ 切割失败,请检查视频文件", None, "", "" random.shuffle(all_clips) clips_per_video = max(1, len(all_clips) // num_output_videos) output_files = [] stored_files = [] print(f"🎬 开始合并生成 {num_output_videos} 个混剪视频...") for i in range(num_output_videos): start_idx = i * clips_per_video end_idx = len(all_clips) if i == num_output_videos - 1 else (start_idx + clips_per_video) selected_clips = all_clips[start_idx:end_idx] if not selected_clips: print(f"⚠️ 无足够片段生成第 {i+1} 个视频,跳过") continue temp_merged = os.path.join(temp_dir, f"merged_{i+1}.mp4") print(f"🔗 正在合并 {len(selected_clips)} 个片段 → {temp_merged}...") if not concat_videos(selected_clips, temp_merged): print(f"❌ 合并失败: 第 {i+1} 个视频") continue timestamp = datetime.now().strftime('%H%M%S') final_output = os.path.join(temp_dir, f"混剪视频_{target_ratio.replace(':', 'x')}_{i+1}_{timestamp}.mp4") print(f"🎬 正在调整比例 {target_ratio} → {final_output}...") if ffmpeg_resize_video(temp_merged, final_output, target_ratio): output_files.append(final_output) stored_path = save_to_storage(final_output) if stored_path: stored_files.append(os.path.basename(stored_path)) print(f"💾 已保存混剪视频: {os.path.basename(stored_path)}") else: print(f"❌ 保存失败: {final_output}") else: print(f"❌ 比例调整失败: 第 {i+1} 个视频") if stored_files: update_storage_config() print(f"✅ 已统一更新储存配置,共保存 {len(stored_files)} 个视频文件") if not output_files: return "❌ 生成混剪视频失败", None, "", "" package_dir = tempfile.mkdtemp() timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') zip_path = os.path.join(package_dir, f"混剪视频包_{target_ratio.replace(':', 'x')}_{timestamp}.zip") print(f"📦 正在打包 {len(output_files)} 个视频文件为 ZIP...") with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: for video_file in output_files: arcname = os.path.basename(video_file) zipf.write(video_file, arcname) print(f" ➤ 已添加: {arcname}") readme = f"""# 混剪视频包 ## 📊 生成信息 - 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - 视频数量: {len(output_files)} 个 - 视频比例: {target_ratio} - 切片时长: {clip_duration} 秒 - 硬件加速: {HARDWARE_ACCEL['encoder']} ## 📁 文件列表 """ for i, vf in enumerate(output_files, 1): size_mb = os.path.getsize(vf) / (1024 * 1024) readme += f"- 混剪视频_{i}.mp4 ({size_mb:.1f}MB)\n" readme += f""" ## 💾 储存信息 所有视频已自动保存到本地储存空间: {', '.join(stored_files)} 视频已按 {target_ratio} 比例优化,可直接发布。 """ zipf.writestr("README.txt", readme.encode('utf-8')) total_size = os.path.getsize(zip_path) / (1024 * 1024) platform_info = "📱 短视频平台" if target_ratio == '9:16' else "🖥️ 长视频平台" end_time = time.time() elapsed = end_time - start_time print(f"🎉 混剪完成!总耗时: {elapsed:.1f} 秒") print(f"📥 下载包大小: {total_size:.1f}MB") print(f"📁 已保存文件: {len(stored_files)} 个") success_msg = f"""✅ 混剪完成并已储存! 📊 **生成统计:** • 🎬 混剪视频: {len(output_files)} 个 • 📐 视频比例: {target_ratio} • 🎯 适合平台: {platform_info} • 📦 下载包大小: {total_size:.1f}MB • ⚡ 硬件加速: {HARDWARE_ACCEL['encoder']} • 🔧 处理线程: {get_optimal_threads()} • ⏱️ 处理耗时: {elapsed:.1f} 秒 💾 **自动储存:** • 📁 储存位置: ~/video_storage/ • 🔥 已保存文件: {len(stored_files)} 个 • ✅ 永久保存,不会丢失 ⬇️ **立即下载:** 点击下方按钮下载打包文件 """ details = f"""🎬 **视频详情:** """ for i, vf in enumerate(output_files, 1): size_mb = os.path.getsize(vf) / (1024 * 1024) details += f"• 混剪视频_{i}: {size_mb:.1f}MB\n" details += f""" 💾 **储存详情:** """ for i, stored_file in enumerate(stored_files, 1): details += f"• 已储存: {stored_file}\n" config, video_list = get_storage_info() storage_info = f"""📊 **储存空间状态:** 💾 总计: {config['total_videos']} 个视频 📦 总大小: {config['total_size_mb']}MB 📁 位置: ~/video_storage/ 📋 **最新文件:** """ for video in video_list[:5]: storage_info += f"• {video['name']} ({video['size_mb']}MB) - {video['modified']}\n" return success_msg, zip_path, details, storage_info except Exception as e: print(f"❌ 处理失败: {str(e)}") return f"❌ 处理失败: {str(e)}", None, "", "" finally: shutil.rmtree(temp_dir, ignore_errors=True) print("🧹 临时文件夹已清理") def refresh_storage_display(): """刷新储存空间显示""" config, video_list = get_storage_info() storage_display = f"""💾 **储存空间概览** 📊 **统计信息:** • 总视频数量: {config['total_videos']} 个 • 总占用空间: {config['total_size_mb']}MB • 储存位置: ~/video_storage/ • 硬件加速: {HARDWARE_ACCEL['encoder']} 📁 **文件列表:** """ if video_list: for video in video_list: storage_display += f"• {video['name']} ({video['size_mb']}MB) - {video['modified']}\n" else: storage_display += "暂无文件\n" file_choices = [video['name'] for video in video_list] return (storage_display, gr.Dropdown(choices=file_choices, label="选择要删除的文件", interactive=True), gr.CheckboxGroup(choices=file_choices, label="选择要下载的文件", interactive=True)) def handle_delete_file(filename): """处理文件删除""" if not filename: return "⚠️ 请选择要删除的文件", refresh_storage_display()[0], refresh_storage_display()[1], refresh_storage_display()[2] result = delete_storage_file(filename) new_display, new_dropdown, new_checkbox = refresh_storage_display() return result, new_display, new_dropdown, new_checkbox def handle_clear_storage(): """处理清空储存""" result = clear_storage() new_display, new_dropdown, new_checkbox = refresh_storage_display() return result, new_display, new_dropdown, new_checkbox def handle_download_all(): """处理一键下载所有""" zip_file, message = download_all_storage() return zip_file, message def handle_download_selected(selected_files): """处理选择下载""" zip_file, message = download_selected_storage(selected_files) return zip_file, message init_storage() def main(): with gr.Blocks(title="FFmpeg混剪+储存+下载管理", theme=gr.themes.Soft()) as demo: gr.HTML(f"""

🎬 FFmpeg 混剪工具 + 储存管理 + 一键下载

长视频切片 → 智能混剪 → 比例调整 → 自动储存 → 一键下载

⚡ 硬件加速: {HARDWARE_ACCEL['encoder']} | 🔧 线程数: {get_optimal_threads()}

""") with gr.Tabs(): # 第一个标签页:视频处理 with gr.TabItem("🎬 视频混剪"): with gr.Row(): with gr.Column(scale=2): video_input = gr.File( label="📤 上传视频文件 (支持多个)", file_types=[".mp4", ".mov", ".avi", ".mkv"], file_count="multiple", height=120 ) with gr.Row(): clip_duration = gr.Number(value=3, label="切片时长(秒)", minimum=1, maximum=3600) num_output = gr.Number(value=3, label="生成数量", minimum=1, maximum=100) ratio_selection = gr.Radio( choices=["9:16", "16:9"], value="9:16", label="📐 视频比例", info="9:16适合抖音快手 | 16:9适合YouTube B站" ) process_btn = gr.Button("🎬 开始混剪并储存", variant="primary", size="lg") with gr.Column(scale=1): status_output = gr.Textbox(label="📊 处理状态", lines=12, interactive=False) with gr.Row(): with gr.Column(): download_file = gr.File(label="📦 下载混剪视频包", interactive=False) with gr.Column(): details_output = gr.Textbox(label="📝 处理详情", lines=12, interactive=False) with gr.Column(): storage_status = gr.Textbox(label="💾 储存状态", lines=12, interactive=False) # 第二个标签页:储存管理 with gr.TabItem("💾 储存管理 + 下载"): with gr.Row(): with gr.Column(scale=2): storage_display = gr.Textbox( label="📁 储存空间", lines=15, interactive=False, value="点击刷新按钮查看储存状态" ) with gr.Column(scale=1): refresh_btn = gr.Button("🔄 刷新储存状态", variant="secondary") gr.Markdown("### ⬇️ 下载管理") download_all_btn = gr.Button("📦 一键下载全部", variant="primary") file_selector_download = gr.CheckboxGroup( choices=[], label="选择要下载的文件", interactive=True ) download_selected_btn = gr.Button("📥 下载选中文件", variant="secondary") storage_download_file = gr.File(label="📦 储存空间下载", interactive=False) gr.Markdown("### 🗑️ 文件管理") file_selector = gr.Dropdown(choices=[], label="选择文件", interactive=True) with gr.Row(): delete_btn = gr.Button("🗑️ 删除文件", variant="secondary") clear_btn = gr.Button("🧹 清空储存", variant="stop") operation_result = gr.Textbox(label="操作结果", lines=4, interactive=False) # 事件绑定 process_btn.click( fn=process_videos_with_storage, inputs=[video_input, clip_duration, num_output, ratio_selection], outputs=[status_output, download_file, details_output, storage_status] ) refresh_btn.click( fn=refresh_storage_display, outputs=[storage_display, file_selector, file_selector_download] ) download_all_btn.click( fn=handle_download_all, outputs=[storage_download_file, operation_result] ) download_selected_btn.click( fn=handle_download_selected, inputs=[file_selector_download], outputs=[storage_download_file, operation_result] ) delete_btn.click( fn=handle_delete_file, inputs=[file_selector], outputs=[operation_result, storage_display, file_selector, file_selector_download] ) clear_btn.click( fn=handle_clear_storage, outputs=[operation_result, storage_display, file_selector, file_selector_download] ) # 页面加载时自动刷新 demo.load( fn=refresh_storage_display, outputs=[storage_display, file_selector, file_selector_download] ) gr.Markdown(f""" --- ### 📖 功能说明 **🎬 视频混剪功能:** - ⚡ 自动切片和随机混剪 - 📐 支持9:16/16:9比例调整 - 📦 打包下载所有生成视频 - 💾 **自动储存到本地目录** - 🚀 **硬件加速: {HARDWARE_ACCEL['encoder']}** - 🔧 **多线程处理: {get_optimal_threads()} 线程** **💾 储存管理功能:** - 📁 所有生成视频自动保存到 `~/video_storage/` - 🔄 实时查看储存空间使用情况 - 📊 显示文件详细信息(大小、时间) **⬇️ 一键下载功能:** - 📦 **一键下载全部**: 打包下载储存空间中所有视频 - 📥 **选择下载**: 勾选特定文件进行批量下载