Spaces:
Sleeping
Sleeping
| # برنامه ادغام ویدیو بدون فاصله (Seamless Video Merger) | |
| import gradio as gr | |
| import os | |
| import tempfile | |
| from moviepy import VideoFileClip, concatenate_videoclips | |
| import numpy as np | |
| from typing import List, Optional | |
| import shutil | |
| def load_video_safe(path): | |
| """بارگذاری امن ویدیو با بررسی صحت فایل""" | |
| try: | |
| clip = VideoFileClip(path) | |
| # بررسی معتبر بودن ویدیو | |
| if clip.duration <= 0 or clip.fps <= 0: | |
| clip.close() | |
| return None | |
| return clip | |
| except Exception as e: | |
| print(f"خطا در بارگذاری {path}: {e}") | |
| return None | |
| def create_smooth_transition(clip1, clip2, transition_type, duration=0.5): | |
| """ایجاد انتقال نرم بین دو ویدیو""" | |
| if transition_type == "بدون انتقال (Cut)": | |
| return clip2 | |
| elif transition_type == "محو شدن (Fade)": | |
| # Fade out clip1 and fade in clip2 | |
| fade_duration = min(duration, clip1.duration * 0.3, clip2.duration * 0.3) | |
| clip1_faded = clip1.fadeout(fade_duration) | |
| clip2_faded = clip2.fadein(fade_duration) | |
| return clip2_faded | |
| elif transition_type == "حل شدن (Dissolve)": | |
| # Cross dissolve effect | |
| dissolve_duration = min(duration, clip1.duration * 0.3, clip2.duration * 0.3) | |
| return clip1.crossfadein(dolve_duration) | |
| elif transition_type == "انتقال سفید (White Flash)": | |
| flash_duration = min(0.3, clip1.duration * 0.2, clip2.duration * 0.2) | |
| return clip1.crossfadein(flash_duration) | |
| elif transition_type == "انتقال سیاه (Black Fade)": | |
| fade_duration = min(duration, clip1.duration * 0.3, clip2.duration * 0.3) | |
| # Create black clip | |
| from moviepy import ColorClip | |
| black_clip = ColorClip( | |
| size=clip1.size, | |
| color=(0, 0, 0), | |
| duration=fade_duration | |
| ).set_fps(clip1.fps) | |
| # Sequence: clip1 -> black -> clip2 | |
| sequence = concatenate_videoclips([clip1, black_clip, clip2]) | |
| return sequence | |
| else: | |
| return clip2 | |
| def merge_videos( | |
| video_files: List[str], | |
| transition_type: str, | |
| transition_duration: float, | |
| remove_gaps: bool, | |
| output_fps: Optional[int], | |
| output_resolution: str, | |
| output_quality: str | |
| ): | |
| """ | |
| ادغام ویدیوها بدون فاصله | |
| Args: | |
| video_files: لیست فایلهای ویدیو | |
| transition_type: نوع انتقال بین ویدیوها | |
| transition_duration: مدت زمان انتقال | |
| remove_gaps: حذف فاصلههای بین ویدیوها | |
| output_fps: فریم ریت خروجی | |
| output_resolution: رزولوشن خروجی | |
| output_quality: کیفیت خروجی | |
| """ | |
| if not video_files or len(video_files) < 1: | |
| return None, "❌ لطفاً حداقل یک ویدیو آپلود کنید" | |
| temp_dir = tempfile.mkdtemp() | |
| output_path = os.path.join(temp_dir, "merged_video.mp4") | |
| try: | |
| # مرحله 1: بارگذاری تمام ویدیوها | |
| print(f"در حال بارگذاری {len(video_files)} ویدیو...") | |
| clips = [] | |
| for i, video_path in enumerate(video_files): | |
| if video_path and os.path.exists(video_path): | |
| clip = load_video_safe(video_path) | |
| if clip: | |
| clips.append((i, clip)) | |
| print(f" ویدیو {i+1}: {clip.duration:.2f} ثانیه، {clip.fps} fps") | |
| if len(clips) == 0: | |
| return None, "❌ هیچ ویدیوی معتبری یافت نشد" | |
| # مرحله 2: مرتبسازی بر اساس ترتیب آپلود | |
| clips.sort(key=lambda x: x[0]) | |
| clips = [clip for _, clip in clips] | |
| # مرحله 3: تنظیم رزولوشن و fps همه ویدیوها به اولین ویدیو | |
| reference_clip = clips[0] | |
| target_size = reference_clip.size | |
| target_fps = output_fps if output_fps else reference_clip.fps | |
| # تنظیم رزولوشن بر اساس انتخاب کاربر | |
| if output_resolution == "720p (1280x720)": | |
| target_size = (1280, 720) | |
| elif output_resolution == "1080p (1920x1080)": | |
| target_size = (1920, 1080) | |
| elif output_resolution == "480p (854x480)": | |
| target_size = (854, 480) | |
| # اگر "همانند اولین ویدیو" باشد، size تغییر نمیکند | |
| processed_clips = [] | |
| for i, clip in enumerate(clips): | |
| # تغییر سایز اگر نیاز باشد | |
| if clip.size != target_size: | |
| clip = clip.resized(new_size=target_size) | |
| # تنظیم fps | |
| if abs(clip.fps - target_fps) > 1: | |
| clip = clip.with_fps(target_fps) | |
| processed_clips.append(clip) | |
| # مرحله 4: حذف فریمهای خالی یا اضافی در انتها/ابتدا | |
| if remove_gaps: | |
| print("در حال حذف فاصلههای اضافی...") | |
| trimmed_clips = [] | |
| for i, clip in enumerate(processed_clips): | |
| # حذف فریمهای سیاه از ابتدا و انتها | |
| start_trim = 0 | |
| end_trim = clip.duration | |
| if end_trim > start_trim: | |
| trimmed_clip = clip.subclipped(start_trim, end_trim) | |
| trimmed_clips.append(trimmed_clip) | |
| else: | |
| trimmed_clips.append(clip) | |
| processed_clips = trimmed_clips | |
| # مرحله 5: ادغام ویدیوها با انتقال نرم | |
| print("در حال ادغام ویدیوها...") | |
| if len(processed_clips) == 1: | |
| final_clip = processed_clips[0] | |
| else: | |
| merged_clips = [processed_clips[0]] | |
| for i in range(1, len(processed_clips)): | |
| clip1 = merged_clips[-1] | |
| clip2 = processed_clips[i] | |
| # کوتاه کردن انتهای clip1 و ابتدای clip2 برای حذف فاصله | |
| if remove_gaps: | |
| # حذف 0.1 ثانیه از انتهای clip1 | |
| if clip1.duration > 0.2: | |
| clip1 = clip1.subclipped(0, clip1.duration - 0.1) | |
| # حذف 0.1 ثانیه از ابتدای clip2 | |
| if clip2.duration > 0.2: | |
| clip2 = clip2.subclipped(0.1, clip2.duration) | |
| # ایجاد انتقال | |
| transition_clip = create_smooth_transition( | |
| clip1, clip2, transition_type, transition_duration | |
| ) | |
| merged_clips.append(transition_clip) | |
| final_clip = concatenate_videoclips(merged_clips, method="compose") | |
| # مرحله 6: تنظیم کیفیت خروجی | |
| print("در حال ذخیره ویدیوی نهایی...") | |
| # انتخاب کیفیت بر اساس انتخاب کاربر | |
| if output_quality == "عالی (Bitrate بالا)": | |
| bitrate = "15M" | |
| elif output_quality == "خوب (Bitrate متوسط)": | |
| bitrate = "8M" | |
| elif output_quality == "معمولی (Bitrate پایین)": | |
| bitrate = "4M" | |
| else: | |
| bitrate = "8M" # پیشفرض | |
| # ✅ اصلاح: حذف پارامتر verbose و logger (در MoviePy نسخه جدید پشتیبانی نمیشود) | |
| # همچنین audio_codec به audio_fps تغییر کرده در نسخههای جدید | |
| try: | |
| # روش اول: MoviePy v2.x (جدید) | |
| final_clip.write_videofile( | |
| output_path, | |
| codec='libx264', | |
| bitrate=bitrate, | |
| fps=target_fps, | |
| audio=True, | |
| audio_codec='aac', | |
| ) | |
| except TypeError: | |
| # روش دوم: MoviePy v1.x (قدیمیتر) | |
| try: | |
| final_clip.write_videofile( | |
| output_path, | |
| codec='libx264', | |
| audio_codec='aac', | |
| bitrate=bitrate, | |
| fps=target_fps, | |
| ) | |
| except TypeError as e: | |
| if "audio_codec" in str(e): | |
| # اگر audio_codec پشتیبانی نمیشود | |
| final_clip.write_videofile( | |
| output_path, | |
| codec='libx264', | |
| bitrate=bitrate, | |
| fps=target_fps, | |
| ) | |
| else: | |
| raise e | |
| # محاسبه آمار | |
| original_duration = sum(clip.duration for clip in clips) | |
| final_duration = final_clip.duration | |
| # پاکسازی حافظه | |
| for clip in clips: | |
| clip.close() | |
| final_clip.close() | |
| # کپی به مسیر قابل دانلود | |
| final_output = os.path.join(tempfile.gettempdir(), "merged_video_final.mp4") | |
| shutil.copy(output_path, final_output) | |
| message = f""" | |
| ✅ ویدیو با موفقیت ادغام شد! | |
| 📊 آمار: | |
| • تعداد ویدیوها: {len(clips)} | |
| • زمان کل (اصلی): {original_duration:.2f} ثانیه | |
| • زمان کل (نهایی): {final_duration:.2f} ثانیه | |
| • نوع انتقال: {transition_type} | |
| • رزولوشن: {target_size[0]}x{target_size[1]} | |
| • فریم ریت: {target_fps} fps | |
| • کیفیت: {output_quality} | |
| """ | |
| return final_output, message | |
| except Exception as e: | |
| import traceback | |
| traceback.print_exc() | |
| return None, f"❌ خطا در پردازش: {str(e)}" | |
| finally: | |
| # پاکسازی فایلهای موقت | |
| try: | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| except: | |
| pass | |
| def get_video_info(video_file): | |
| """دریافت اطلاعات یک ویدیو""" | |
| if not video_file or not os.path.exists(video_file): | |
| return "❌ فایل یافت نشد" | |
| try: | |
| clip = VideoFileClip(video_file) | |
| info = f""" | |
| 📹 اطلاعات ویدیو: | |
| • مدت زمان: {clip.duration:.2f} ثانیه | |
| • رزولوشن: {clip.size[0]}x{clip.size[1]} | |
| • فریم ریت: {clip.fps} fps | |
| • صدا: {'دارد' if clip.audio else 'ندارد'} | |
| """ | |
| clip.close() | |
| return info | |
| except Exception as e: | |
| return f"❌ خطا: {str(e)}" | |
| # ایجاد تم سفارشی | |
| custom_theme = gr.themes.Soft( | |
| primary_hue="blue", | |
| secondary_hue="indigo", | |
| neutral_hue="slate", | |
| font=gr.themes.GoogleFont("Vazirmatn"), | |
| text_size="lg", | |
| spacing_size="lg", | |
| radius_size="md" | |
| ).set( | |
| button_primary_background_fill="*primary_600", | |
| button_primary_background_fill_hover="*primary_700", | |
| block_title_text_weight="600", | |
| body_text_weight="400", | |
| ) | |
| # CSS سفارشی برای پشتیبانی از زبان فارسی | |
| custom_css = """ | |
| .gradio-container { | |
| direction: rtl; | |
| font-family: 'Vazirmatn', 'Tahoma', sans-serif !important; | |
| } | |
| .gap-4 { | |
| gap: 1rem !important; | |
| } | |
| .prose { | |
| text-align: right !important; | |
| } | |
| #error_message { | |
| background-color: #fef2f2 !important; | |
| border-color: #ef4444 !important; | |
| } | |
| #success_message { | |
| background-color: #f0fdf4 !important; | |
| border-color: #22c55e !important; | |
| } | |
| """ | |
| # ساخت رابط کاربری | |
| # ✅ GRADIO 6: gr.Blocks() بدون پارامتر | |
| with gr.Blocks() as demo: | |
| # هدر با لینک anycoder | |
| gr.HTML(""" | |
| <div style="text-align: center; padding: 10px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; margin-bottom: 20px;"> | |
| <h1 style="color: white; margin: 0; font-size: 2em;">🎬 ادغام ویدیو بدون فاصله</h1> | |
| <p style="color: rgba(255,255,255,0.9); margin: 10px 0 0 0;">Video Merger - Seamless Clips</p> | |
| <p style="color: rgba(255,255,255,0.7); font-size: 0.9em; margin: 5px 0 0 0;"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="color: #ffd700;">⭐ Built with anycoder</a> | |
| </p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| # بخش آپلود ویدیوها | |
| gr.Markdown("## 📤 آپلود ویدیوها", elem_classes=["prose"]) | |
| video_uploader = gr.File( | |
| label="انتخاب ویدیوها (میتوانید چندین فایل انتخاب کنید)", | |
| file_types=["video"], | |
| file_count="multiple", | |
| height=150, | |
| elem_id="video_uploader" | |
| ) | |
| # نمایش تعداد ویدیوهای انتخاب شده | |
| video_count = gr.Number(value=0, label="تعداد ویدیوها", interactive=False) | |
| video_uploader.change( | |
| lambda files: len(files) if files else 0, | |
| video_uploader, | |
| video_count | |
| ) | |
| # اطلاعات ویدیوها | |
| gr.Markdown("### ℹ️ اطلاعات ویدیوهای انتخاب شده:") | |
| video_info = gr.Textbox( | |
| value="", | |
| label="", | |
| lines=5, | |
| interactive=False, | |
| elem_id="video_info" | |
| ) | |
| with gr.Column(scale=1): | |
| # تنظیمات انتقال | |
| gr.Markdown("## ⚙️ تنظیمات", elem_classes=["prose"]) | |
| transition_type = gr.Dropdown( | |
| choices=[ | |
| "بدون انتقال (Cut)", | |
| "محو شدن (Fade)", | |
| "حل شدن (Dissolve)", | |
| "انتقال سیاه (Black Fade)", | |
| "انتقال سفید (White Flash)" | |
| ], | |
| value="بدون انتقال (Cut)", | |
| label="نوع انتقال بین ویدیوها", | |
| info="برای حذف کامل فاصله، 'بدون انتقال' را انتخاب کنید" | |
| ) | |
| transition_duration = gr.Slider( | |
| minimum=0.1, | |
| maximum=2.0, | |
| value=0.3, | |
| step=0.1, | |
| label="مدت زمان انتقال (ثانیه)", | |
| info="فقط برای انتقالهای Fade و Dissolve" | |
| ) | |
| remove_gaps = gr.Checkbox( | |
| value=True, | |
| label="حذف فاصلههای اضافی", | |
| info="فریمهای خالی ابتدا و انتهای هر کلیپ را حذف میکند" | |
| ) | |
| # تنظیمات خروجی | |
| gr.Markdown("### 🎯 تنظیمات خروجی", elem_classes=["prose"]) | |
| output_resolution = gr.Dropdown( | |
| choices=[ | |
| "همانند اولین ویدیو", | |
| "480p (854x480)", | |
| "720p (1280x720)", | |
| "1080p (1920x1080)" | |
| ], | |
| value="همانند اولین ویدیو", | |
| label="رزولوشن خروجی" | |
| ) | |
| output_fps = gr.Dropdown( | |
| choices=["24", "30", "60", "همانند اولین ویدیو"], | |
| value="همانند اولین ویدیو", | |
| label="فریم ریت خروجی" | |
| ) | |
| output_quality = gr.Dropdown( | |
| choices=["خوب (Bitrate متوسط)", "عالی (Bitrate بالا)", "معمولی (Bitrate پایین)"], | |
| value="خوب (Bitrate متوسط)", | |
| label="کیفیت خروجی" | |
| ) | |
| # دکمه شروع ادغام | |
| merge_btn = gr.Button( | |
| "🚀 ادغام ویدیوها", | |
| variant="primary", | |
| size="lg", | |
| elem_id="merge_btn" | |
| ) | |
| # نوار پیشرفت | |
| progress_bar = gr.Progress() | |
| # پیام وضعیت | |
| status_message = gr.Textbox( | |
| label="پیام سیستم", | |
| lines=3, | |
| interactive=False, | |
| elem_id="status_message" | |
| ) | |
| # ویدیوی خروجی | |
| gr.Markdown("## 🎉 ویدیوی ادغام شده", elem_classes=["prose"]) | |
| output_video = gr.Video( | |
| label="ویدیوی نهایی", | |
| height=400, | |
| format="mp4" | |
| ) | |
| # دکمه دانلود | |
| download_btn = gr.DownloadButton( | |
| value=None, | |
| label="⬇️ دانلود ویدیو", | |
| variant="secondary", | |
| size="lg", | |
| visible=False, | |
| elem_id="download_btn" | |
| ) | |
| # تابع ادغام با progress | |
| def merge_with_progress( | |
| video_files, transition_type, transition_duration, | |
| remove_gaps, output_fps, output_resolution, output_quality, progress=gr.Progress() | |
| ): | |
| progress(0, desc="شروع...") | |
| if not video_files or len(video_files) == 0: | |
| return None, None, "❌ لطفاً ویدیو انتخاب کنید", gr.update(visible=False) | |
| # تبدیل fps | |
| fps_value = None | |
| if output_fps != "همانند اولین ویدیو": | |
| fps_value = int(output_fps) | |
| progress(0.1, desc="در حال پردازش...") | |
| # فراخوانی تابع اصلی | |
| output_path, message = merge_videos( | |
| video_files, | |
| transition_type, | |
| transition_duration, | |
| remove_gaps, | |
| fps_value, | |
| output_resolution, | |
| output_quality | |
| ) | |
| progress(1.0, desc="تکمیل!") | |
| if output_path: | |
| return output_path, output_path, message, gr.update(value=output_path, visible=True) | |
| else: | |
| return None, None, message, gr.update(visible=False) | |
| # اتصال رویدادها | |
| merge_btn.click( | |
| merge_with_progress, | |
| inputs=[ | |
| video_uploader, transition_type, transition_duration, | |
| remove_gaps, output_fps, output_resolution, output_quality | |
| ], | |
| outputs=[output_video, download_btn, status_message, download_btn], | |
| show_progress="full" | |
| ) | |
| # راهنما | |
| with gr.Accordion("📖 راهنما و نکات مهم", open=True): | |
| gr.Markdown(""" | |
| ### 🔧 نحوه استفاده: | |
| 1. **آپلود ویدیوها**: روی دکمه کلیک کنید و ویدیوهای خود را انتخاب کنید (میتوانید چندین فایل را همزمان انتخاب کنید) | |
| 2. **ترتیب ویدیوها**: ترتیب آپلود = ترتیب ادغام | |
| 3. **نوع انتقال**: | |
| - **بدون انتقال (Cut)**: ویدیوها مستقیم به هم وصل میشوند (بهترین برای حذف فاصله) | |
| - **Fade/Dissolve**: انتقال نرم بین ویدیوها | |
| 4. **حذف فاصلهها**: تیک "حذف فاصلههای اضافی" را فعال کنید | |
| 5. **ادغام**: روی دکمه "ادغام ویدیوها" کلیک کنید | |
| 6. **دانلود**: ویدیوی نهایی را دانلود کنید | |
| ### ⚡ نکات مهم: | |
| - فرمت خروجی: **MP4** | |
| - کدک: **H.264** (سازگار با تمام پلتفرمها) | |
| - صدا: **AAC** (22050Hz) | |
| - حداکثر اندازه فایل: 2GB | |
| """) | |
| # ✅ GRADIO 6: تمام پارامترها در launch() قرار میگیرند | |
| demo.launch( | |
| theme=custom_theme, | |
| css=custom_css, | |
| footer_links=[ | |
| {"label": "Built with anycoder", "url": "https://huggingface.co/spaces/akhaliq/anycoder"} | |
| ], | |
| server_port=7860, | |
| debug=True | |
| ) |