| 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 |
|
|
| |
| STORAGE_DIR = os.path.expanduser("~/video_storage") |
| STORAGE_CONFIG = os.path.join(STORAGE_DIR, "storage_config.json") |
|
|
| 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 ffmpeg_cut_video(input_path, start_time, duration, output_path): |
| """[优化] 快速的视频切割,使用 superfast 预设""" |
| command = [ |
| 'ffmpeg', '-i', input_path, '-ss', str(start_time), '-t', str(duration), |
| '-c:v', 'libx264', '-preset', 'superfast', '-crf', '28', |
| '-c:a', 'aac', '-b:a', '128k', '-avoid_negative_ts', 'make_zero', '-y', output_path |
| ] |
| result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| return result.returncode == 0 and os.path.exists(output_path) |
|
|
| def ffmpeg_resize_video(input_path, output_path, target_ratio): |
| """[优化] 快速的比例调整,使用 superfast 预设""" |
| 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 = [ |
| 'ffmpeg', '-i', input_path, '-vf', filter_complex, |
| '-c:v', 'libx264', '-preset', 'superfast', '-crf', '28', '-c:a', 'copy', '-y', output_path |
| ] |
| result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| return result.returncode == 0 and os.path.exists(output_path) |
|
|
| 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() |
| |
| command = ['ffmpeg', '-f', 'concat', '-safe', '0', '-i', list_file.name, '-c', 'copy', '-y', output_path] |
| result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| return result.returncode == 0 and 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()) |
| 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") |
|
|
| if ffmpeg_cut_video(video_path, start, duration, clip_path): |
| clips.append(clip_path) |
| else: |
| print(f"切割视频 {video_path} 的第 {count} 个片段失败") |
|
|
| 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, "", "" |
| |
| temp_dir = tempfile.mkdtemp() |
| |
| try: |
| all_clips = [] |
|
|
| |
| 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 |
| } |
| |
| |
| for future in concurrent.futures.as_completed(future_to_video): |
| try: |
| video_clips = future.result() |
| all_clips.extend(video_clips) |
| except Exception as exc: |
| video_file = future_to_video[future] |
| print(f'处理视频 {video_file.name} 时发生异常: {exc}') |
| |
|
|
| 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 = [] |
| |
| 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: |
| continue |
| |
| temp_merged = os.path.join(temp_dir, f"merged_{i+1}.mp4") |
| if not concat_videos(selected_clips, temp_merged): |
| continue |
| |
| timestamp = datetime.now().strftime('%H%M%S') |
| final_output = os.path.join(temp_dir, f"混剪视频_{target_ratio.replace(':', 'x')}_{i+1}_{timestamp}.mp4") |
| |
| 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)) |
| |
| |
| if stored_files: |
| update_storage_config() |
| |
| |
| 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") |
| |
| 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) |
| |
| readme = f"""# 混剪视频包 |
| |
| ## 📊 生成信息 |
| - 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} |
| - 视频数量: {len(output_files)} 个 |
| - 视频比例: {target_ratio} |
| - 切片时长: {clip_duration} 秒 |
| |
| ## 📁 文件列表 |
| """ |
| 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 "🖥️ 长视频平台" |
| |
| success_msg = f"""✅ 混剪完成并已储存! |
| |
| 📊 **生成统计:** |
| • 🎬 混剪视频: {len(output_files)} 个 |
| • 📐 视频比例: {target_ratio} |
| • 🎯 适合平台: {platform_info} |
| • 📦 下载包大小: {total_size:.1f}MB |
| |
| 💾 **自动储存:** |
| • 📁 储存位置: ~/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: |
| return f"❌ 处理失败: {str(e)}", None, "", "" |
| |
| finally: |
| shutil.rmtree(temp_dir, ignore_errors=True) |
|
|
| def refresh_storage_display(): |
| """刷新储存空间显示""" |
| config, video_list = get_storage_info() |
| |
| storage_display = f"""💾 **储存空间概览** |
| |
| 📊 **统计信息:** |
| • 总视频数量: {config['total_videos']} 个 |
| • 总占用空间: {config['total_size_mb']}MB |
| • 储存位置: ~/video_storage/ |
| |
| 📁 **文件列表:** |
| """ |
| |
| 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(""" |
| <div style="text-align: center; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; color: white; margin-bottom: 20px;"> |
| <h1>🎬 FFmpeg 混剪工具 + 储存管理 + 一键下载</h1> |
| <p style="margin: 10px 0 0 0;">长视频切片 → 智能混剪 → 比例调整 → 自动储存 → 一键下载</p> |
| </div> |
| """) |
| |
| 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(""" |
| --- |
| ### 📖 功能说明 |
| |
| **🎬 视频混剪功能:** |
| - ⚡ 自动切片和随机混剪 |
| - 📐 支持9:16/16:9比例调整 |
| - 📦 打包下载所有生成视频 |
| - 💾 **自动储存到本地目录** |
| |
| **💾 储存管理功能:** |
| - 📁 所有生成视频自动保存到 `~/video_storage/` |
| - 🔄 实时查看储存空间使用情况 |
| - 📊 显示文件详细信息(大小、时间) |
| |
| **⬇️ 一键下载功能:** |
| - 📦 **一键下载全部**: 打包下载储存空间中所有视频 |
| - 📥 **选择下载**: 勾选特定文件进行批量下载 |
| - 🗂️ **自动清单**: 下载包含详细文件清单 |
| - ⚡ **快速打包**: 自动压缩,节省下载时间 |
| |
| **🗑️ 文件管理功能:** |
| - 🗑️ 支持单个文件删除 |
| - 🧹 支持清空全部储存文件 |
| - 📱 灵活的文件管理操作 |
| |
| **🔥 使用场景:** |
| - **批量备份**: 一键下载所有混剪作品 |
| - **选择性导出**: 只下载需要的特定视频 |
| - **移动设备**: 下载到手机/平板继续编辑 |
| - **分享协作**: 打包分享给团队成员 |
| - **存档管理**: 定期下载备份到云盘 |
| |
| **⚠️ 注意事项:** |
| - 下载文件为ZIP格式,需要解压使用 |
| - 一键下载包含储存空间中所有视频文件 |
| - 选择下载可以精确控制需要的文件 |
| - 下载包自动包含详细的文件清单 |
| """) |
| |
| demo.launch() |
|
|
| if __name__ == "__main__": |
| main() |