import gradio as gr from pydub import AudioSegment import requests import os import uuid import re import mimetypes import tempfile # برای مدیریت فایل‌های موقت # --- تنظیمات FFmpeg/ffprobe (برای محیط‌های محلی) --- # در محیط‌های Production مانند Hugging Face Spaces، معمولاً FFmpeg از قبل نصب شده است. # این خطوط تنها در صورتی نیاز است که با مشکل "FFmpeg or AVConvNotFound" مواجه شوید # و FFmpeg را در مسیرهای غیر استاندارد نصب کرده باشید. # PARENT_DIR = os.path.dirname(os.path.abspath(__file__)) # FFMPEG_PATH = os.path.join(PARENT_DIR, "ffmpeg", "bin", "ffmpeg.exe") # مثال برای ویندوز # FFPROBE_PATH = os.path.join(PARENT_DIR, "ffmpeg", "bin", "ffprobe.exe") # مثال برای ویندوز # # if os.path.exists(FFMPEG_PATH): # AudioSegment.converter = FFMPEG_PATH # print(f"FFmpeg path set to: {FFMPEG_PATH}") # if os.path.exists(FFPROBE_PATH): # AudioSegment.ffprobe = FFPROBE_PATH # print(f"FFprobe path set to: {FFPROBE_PATH}") # else: # print("FFmpeg/FFprobe paths not explicitly set. Pydub will look in system PATH.") # یک تابع کمکی برای بررسی وجود FFmpeg def check_ffmpeg_presence(): try: from pydub.utils import get_prober_name, get_encoder_name # تلاش برای فراخوانی ffprobe تا بررسی کنیم آیا در دسترس است یا نه output = os.popen(f"{get_prober_name()} -version").read() if "ffprobe" in output or "avprobe" in output: print("FFprobe is available.") return True else: print("FFprobe not found in system PATH or configured path.") return False except Exception as e: print(f"Error checking FFmpeg/FFprobe: {e}") return False # بررسی در ابتدای اجرا if not check_ffmpeg_presence(): print("WARNING: FFmpeg/ffprobe might not be correctly installed or configured. Audio processing may fail.") # می‌توانید اینجا یک خطای Gradio به کاربر نشان دهید # gr.Warning("FFmpeg/ffprobe not found. Please ensure it's installed and accessible.") def download_audio(url: str, output_dir: str) -> str | None: """ فایل صوتی را از URL دانلود می‌کند. :param url: URL فایل صوتی. :param output_dir: دایرکتوری برای ذخیره فایل دانلود شده. :return: مسیر کامل فایل دانلود شده یا None در صورت خطا. """ try: response = requests.get(url, stream=True, timeout=30) response.raise_for_status() # استفاده از Content-Disposition برای نام فایل اگر موجود باشد content_disposition = response.headers.get('Content-Disposition') if content_disposition: fname_match = re.search(r'filename\*?=(?:UTF-8\'\')?\"?([^\"]+)\"?', content_disposition) if fname_match: original_filename = fname_match.group(1).encode('latin-1').decode('utf-8') # پاکسازی نام فایل برای جلوگیری از مشکلات مسیر original_filename = re.sub(r'[\\/:*?"<>|]', '_', original_filename) # اطمینان حاصل شود که پسوند دارد if not os.path.splitext(original_filename)[1]: original_filename += mimetypes.guess_extension(response.headers.get('Content-Type', ''), strict=False) or '.tmp' temp_filename = f"{uuid.uuid4().hex}_{original_filename}" else: temp_filename = f"temp_download_{uuid.uuid4().hex}.tmp" else: # تخمین پسوند از Content-Type یا URL content_type = response.headers.get('Content-Type', '') ext = mimetypes.guess_extension(content_type, strict=False) if not ext: path_parts = os.path.splitext(url.split('?')[0]) if len(path_parts) > 1 and path_parts[1]: ext = path_parts[1].lower() else: ext = ".tmp" # پسوند موقت برای pydub تا خودش تشخیص دهد # ساخت نام فایل موقت (با پسوند مربوطه) temp_filename = f"temp_download_{uuid.uuid4().hex}{ext}" output_path = os.path.join(output_dir, temp_filename) with open(output_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) print(f"Successfully downloaded {url} to {output_path}") return output_path except requests.exceptions.Timeout: print(f"Error downloading {url}: Request timed out after 30 seconds.") return None except requests.exceptions.RequestException as e: print(f"Network or HTTP error downloading {url}: {e}") return None except Exception as e: print(f"An unexpected error occurred during download of {url}: {e}") return None def merge_audio_files_sequential(audio_file_paths: list[str], output_file_path: str) -> bool: """ چندین فایل صوتی را به ترتیب ادغام می‌کند. همه فایل‌ها به MP3 خروجی گرفته می‌شوند. :param audio_file_paths: لیستی از مسیرهای فایل‌های صوتی. :param output_file_path: مسیر کامل برای ذخیره فایل ادغام شده (خروجی همیشه MP3 خواهد بود). :return: True اگر ادغام موفق بود، False در غیر این صورت. """ if not audio_file_paths: print("No audio files provided for merging.") return False combined_audio = None successful_merges = 0 try: # بارگذاری اولین فایل try: combined_audio = AudioSegment.from_file(audio_file_paths[0]) print(f"Successfully loaded initial audio from: {audio_file_paths[0]}") successful_merges = 1 except Exception as e: print(f"Error loading first audio file {audio_file_paths[0]}: {e}") return False # ادغام بقیه فایل‌ها for i, file_path in enumerate(audio_file_paths[1:]): try: audio = AudioSegment.from_file(file_path) combined_audio += audio print(f"Successfully merged {file_path}") successful_merges += 1 except Exception as e: print(f"Error merging audio from {file_path}: {e}. Skipping this file.") # در اینجا می‌توانیم انتخاب کنیم که آیا کل عملیات را متوقف کنیم یا ادامه دهیم. # برای این کاربرد، ادامه دادن معقول‌تر است (ادغام بقیه فایل‌های موفق). pass # ادامه به فایل بعدی حتی اگر این یکی موفق نبود if successful_merges < len(audio_file_paths): print(f"Warning: Only {successful_merges} out of {len(audio_file_paths)} files were successfully loaded/merged.") if successful_merges == 0: print("No audio segments were successfully processed for merging.") return False # خروجی گرفتن نهایی به فرمت MP3 combined_audio.export(output_file_path, format="mp3") print(f"Successfully exported final merged file to {output_file_path}") return True except Exception as e: print(f"An unexpected error occurred during audio merging process: {e}") return False def process_audio_links_gradio(urls_input: str) -> tuple[str, str | None]: """ تابع اصلی Gradio برای پردازش لینک‌های صوتی. """ urls = [url.strip() for url in re.split(r'[\n,]+', urls_input) if url.strip()] if not urls: return "لطفاً حداقل یک لینک فایل صوتی معتبر وارد کنید.", None if len(urls) < 2: return "برای ادغام، لطفاً حداقل دو لینک فایل صوتی وارد کنید.", None status_message = "" output_audio_path = None downloaded_files_paths = [] # استفاده از NamedTemporaryFile برای دایرکتوری موقت # این دایرکتوری به طور خودکار پس از خروج از context manager حذف می‌شود. with tempfile.TemporaryDirectory() as request_temp_dir: print(f"Using temporary directory: {request_temp_dir}") try: for i, url in enumerate(urls): status_message += f"در حال دانلود فایل {i+1} از: {url}\n" gr.Info(f"Downloading file {i+1} of {len(urls)}...") downloaded_path = download_audio(url, request_temp_dir) if downloaded_path: downloaded_files_paths.append(downloaded_path) else: return f"خطا در دانلود فایل {i+1} از '{url}'. لطفاً لینک را بررسی کنید. ممکن است فایل صوتی نباشد یا دسترسی به آن ممکن نباشد.", None if not downloaded_files_paths: return "هیچ فایلی با موفقیت دانلود نشد. لطفاً لینک‌ها را بررسی کنید.", None # ترتیب دهی فایل ها بر اساس نام (برای تضمین ترتیب ادغام) downloaded_files_paths.sort() status_message += "فایل‌ها با موفقیت دانلود شدند. در حال ادغام...\n" gr.Info("Files downloaded. Merging audio segments...") output_file_name_final = f"merged_output_{uuid.uuid4().hex}.mp3" output_file_path_final = os.path.join(request_temp_dir, output_file_name_final) if merge_audio_files_sequential(downloaded_files_paths, output_file_path_final): status_message += f"فایل‌ها با موفقیت ادغام شدند. فایل خروجی: {output_file_path_final}\n" output_audio_path = output_file_path_final else: status_message += "خطا در ادغام فایل‌ها. (جزئیات خطا در کنسول سرور)\n" except Exception as e: status_message += f"خطای پیش‌بینی نشده در طول پردازش: {e}\n" print(f"Unhandled exception in process_audio_links_gradio: {e}") finally: # tempfile.TemporaryDirectory به صورت خودکار دایرکتوری را پاک می‌کند. # فقط باید مطمئن شویم که Gradio به فایل `output_audio_path` اشاره می‌کند # و gradigo خودش آن فایل را برای دانلود در اختیار قرار می‌دهد. print(f"Temporary directory {request_temp_dir} will be removed.") return status_message, output_audio_path # تعریف رابط Gradio # (بخش interface بدون تغییرات عمده) iface = gr.Interface( fn=process_audio_links_gradio, inputs=[ gr.Textbox( label="لینک‌های فایل صوتی (MP3, WAV و غیره) - هر لینک در یک خط یا با کاما جدا کنید", placeholder="مثال:\nhttps://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3\nhttps://file-examples.com/storage/fe/2017/11/file_example_WAV_1MG.wav\n...", lines=5 ), ], outputs=[ gr.Textbox(label="وضعیت", interactive=False), gr.Audio(label="فایل صوتی ادغام شده", type="filepath") ], title="🎙️ ادغام کننده چندین فایل صوتی از طریق لینک (MP3, WAV و غیره) 🎵", description="لینک فایل‌های صوتی خود را (هر لینک در یک خط جدید یا با کاما جدا شده) در کادر زیر وارد کنید تا به صورت یک فایل **MP3** ادغام شوند.", allow_flagging="never", theme=gr.themes.Soft(), css=""" body { font-family: 'Vazirmatn', sans-serif; direction: rtl; text-align: right; } h1, h2, h3, h4, h5, h6 { text-align: center; } .gr-textbox label, .gr-audio label { text-align: right; width: 100%; display: block; } .gradio-container { max-width: 800px; margin: auto; padding: 20px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); border-radius: 8px; } .gr-button { background-color: #4CAF50; color: white; } """ ) if __name__ == "__main__": iface.launch(share=True)