File size: 12,977 Bytes
319ea69
c296f8a
cd88e5f
39ac26a
cd88e5f
9ddae4b
 
 
319ea69
9ddae4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd88e5f
9ddae4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd88e5f
1d71064
 
 
9ddae4b
cd88e5f
 
1d71064
 
cd88e5f
9ddae4b
 
 
 
 
 
 
 
 
 
 
 
1d71064
9ddae4b
 
 
 
 
 
 
 
 
 
 
 
 
 
1d71064
9ddae4b
1d71064
cd88e5f
 
 
1d71064
cd88e5f
9ddae4b
 
cd88e5f
9ddae4b
 
cd88e5f
9ddae4b
 
cd88e5f
 
9ddae4b
cd88e5f
1d71064
cd88e5f
1d71064
9ddae4b
1d71064
cd88e5f
 
1d71064
9ddae4b
cd88e5f
 
 
9ddae4b
 
c296f8a
cd88e5f
9ddae4b
 
 
 
 
 
 
cd88e5f
 
1d71064
9ddae4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd88e5f
1d71064
cd88e5f
9ddae4b
cd88e5f
319ea69
9ddae4b
cd88e5f
 
1d71064
cd88e5f
9ddae4b
cd88e5f
 
 
 
1d71064
cd88e5f
 
9ddae4b
cd88e5f
 
 
1d71064
cd88e5f
9ddae4b
 
 
 
 
 
 
 
 
 
 
 
 
cd88e5f
9ddae4b
 
 
 
 
cd88e5f
9ddae4b
 
cd88e5f
9ddae4b
 
cd88e5f
9ddae4b
 
 
 
 
cd88e5f
9ddae4b
 
 
 
 
 
 
 
cd88e5f
 
 
 
9ddae4b
cd88e5f
1d71064
cd88e5f
 
1d71064
9ddae4b
1d71064
cd88e5f
 
 
 
1d71064
cd88e5f
1d71064
 
cd88e5f
9ddae4b
cd88e5f
 
 
 
 
 
 
 
 
 
 
 
9ddae4b
cd88e5f
 
 
 
 
3a12f33
1d71064
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
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)