File size: 14,458 Bytes
75b265b
87fff27
 
 
75b265b
87fff27
8a2c988
87fff27
 
 
 
 
 
 
 
8a2c988
75b265b
87fff27
ab2114d
87fff27
 
ab2114d
c6b7b8e
 
34f1d12
4f0b0e0
 
 
 
 
 
 
787368a
 
 
 
 
 
 
 
2571799
 
 
 
 
 
 
 
 
c6b7b8e
bf05f28
87fff27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ab2114d
87fff27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
948d177
87fff27
 
 
 
 
35381b3
87fff27
 
 
ab2114d
87fff27
 
 
ab2114d
87fff27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35381b3
87fff27
 
b078ee8
35381b3
87fff27
 
 
 
 
 
35381b3
 
 
87fff27
 
 
 
 
35381b3
87fff27
35381b3
 
87fff27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75b265b
 
87fff27
 
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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
import os
import uuid
import shutil
import asyncio
import random
import requests
import zipfile
import time
import numpy as np
import soundfile as sf
import librosa
from pathlib import Path
from fastapi import FastAPI, UploadFile, File, Form, Request, BackgroundTasks
from fastapi.responses import FileResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from gradio_client import Client, handle_file

app = FastAPI()

# --- لیست کارگرها ---
# اینجا بعداً می‌توانید ۵۰ تا کارگر اضافه کنید
WORKER_URLS = [
    "https://ezmary-rvc-zeroworker1.hf.space",
    "https://ezmary-rvc-zeroworker2.hf.space",
    "https://ezmary-rvc-zeroworker3.hf.space",
    "https://rvisi-rvskareqar4.hf.space",
    "https://rvisi-rvskareqar5.hf.space",
    "https://rvisi-rvskareqar6.hf.space",
    "https://rvisi-rvskareqar7.hf.space",
    "https://rvisi-rvskareqar8.hf.space",
    "https://rvisi-rvskareqar9.hf.space",
    "https://rvisi-rvskareqar10.hf.space",
    "https://rvisi-rvskareqar11.hf.space",
    "https://rvisiasa-rvc-zeroworker12.hf.space",
    "https://rvisiasa-rvc-zeroworker13.hf.space",
    "https://rvisiasa-rvc-zeroworker14.hf.space",
    "https://rvisiasa-rvc-zeroworker15.hf.space",
    "https://rvisiasa-rvc-zeroworker16.hf.space",
    "https://rvisiasa-rvc-zeroworker17.hf.space",
    "https://rvisiasa-rvc-zeroworker18.hf.space",
    "https://rvisiasa-rvc-zeroworker19.hf.space",
    "https://rvis84651-rvc-zeroworker20.hf.space",
    "https://rvis84651-rvc-zeroworker21.hf.space",
    "https://rvis84651-rvc-zeroworker22.hf.space",
    "https://rvis84651-rvc-zeroworker23.hf.space",
    "https://rvis84651-rvc-zeroworker24.hf.space",
    "https://rvis84651-rvc-zeroworker25.hf.space",
    "https://rvis84651-rvc-zeroworker26.hf.space",
    "https://rvis84651-rvc-zeroworker27.hf.space"
]

BASE_DIR = Path(__file__).parent.absolute()
TEMP_DIR = BASE_DIR / "temp"
RESULTS_DIR = BASE_DIR / "results"
DOWNLOADS_DIR = BASE_DIR / "downloads"

TEMP_DIR.mkdir(exist_ok=True)
RESULTS_DIR.mkdir(exist_ok=True)
DOWNLOADS_DIR.mkdir(exist_ok=True)

templates = Jinja2Templates(directory="templates")
JOBS = {}

# --- کلاس مدیریت اتمیک کارگرها (جدید) ---
class AtomicWorkerManager:
    def __init__(self, urls):
        self.urls = urls
        self.total = len(urls)
        self.current_index = 0
        self.lock = asyncio.Lock() # قفل برای جلوگیری از تداخل درخواست‌ها

    async def get_next_worker(self):
        """
        این تابع به صورت اتمیک کارگر بعدی را برمی‌گرداند 
        و نوبت را جلو می‌برد.
        """
        async with self.lock:
            worker = self.urls[self.current_index]
            # جلو بردن نوبت (چرخشی)
            self.current_index = (self.current_index + 1) % self.total
            return worker

# ایجاد نمونه مدیر کارگر
worker_manager = AtomicWorkerManager(WORKER_URLS)


# --- تابع دانلود ---
def download_file(url, dest_folder):
    try:
        url = url.strip()
        if "huggingface.co" in url:
            url = url.replace("/blob/", "/resolve/").replace("/tree/", "/resolve/")
            if "?download=true" not in url:
                url += "&download=true" if "?" in url else "?download=true"

        filename = url.split('/')[-1].split('?')[0]
        if not any(filename.endswith(ext) for ext in ['.pth', '.index', '.zip']):
            if "zip" in url.lower(): filename = f"model_{random.randint(1000,9999)}.zip"
            else: filename = f"model_{random.randint(1000,9999)}.pth"
            
        dest_path = dest_folder / filename
        if dest_path.exists():
            try: os.remove(dest_path)
            except: pass

        print(f"Downloading: {url}")
        headers = {'User-Agent': 'Mozilla/5.0'}
        with requests.get(url, stream=True, headers=headers, allow_redirects=True) as r:
            r.raise_for_status()
            if 'text/html' in r.headers.get('Content-Type', '').lower(): raise Exception("لینک HTML است.")
            with open(dest_path, 'wb') as f:
                for chunk in r.iter_content(chunk_size=8192): f.write(chunk)
        
        if dest_path.stat().st_size < 1024 * 10:
            os.remove(dest_path)
            raise Exception("فایل نامعتبر است.")
        return dest_path
    except Exception as e:
        if 'dest_path' in locals() and dest_path.exists():
            try: os.remove(dest_path)
            except: pass
        raise e

# --- تابع برش هوشمند ---
def smart_split_audio(audio_path, job_id):
    print(f"Analyzing audio: {audio_path}")
    try:
        y, sr = librosa.load(str(audio_path), sr=None)
        duration = librosa.get_duration(y=y, sr=sr)
        
        if duration <= 30:
            return [str(audio_path)]

        chunks_info = []
        min_segment = 6    
        max_segment = 30   
        
        total_samples = len(y)
        current_sample = 0
        chunk_idx = 0
        
        while current_sample < total_samples:
            start_search = current_sample + (min_segment * sr)
            end_search = current_sample + (max_segment * sr)
            
            if end_search >= total_samples:
                split_point = total_samples
            else:
                search_region = y[start_search:end_search]
                rms = librosa.feature.rms(y=search_region, frame_length=2048, hop_length=512)[0]
                min_rms_idx = np.argmin(rms)
                split_offset = min_rms_idx * 512
                split_point = start_search + split_offset

            chunk_audio = y[current_sample:split_point]
            chunk_name = f"{job_id}_chunk_{chunk_idx}.wav"
            chunk_path = TEMP_DIR / chunk_name
            
            sf.write(str(chunk_path), chunk_audio, sr)
            chunks_info.append(str(chunk_path))
            
            current_sample = split_point
            chunk_idx += 1
            
        print(f"Split into {len(chunks_info)} chunks.")
        return chunks_info
    except Exception as e:
        print(f"Split Error: {e}")
        return [str(audio_path)]

# --- پردازشگر یک قطعه (با تلاش بی‌نهایت) ---
def process_single_chunk_sync(chunk_path, model_path, index_path, params, assigned_worker_url, all_worker_urls):
    """
    1. ابتدا با کارگر تعیین شده (assigned_worker_url) تلاش می‌کند.
    2. اگر نشد، وارد چرخه تلاش مجدد با سایر کارگرها می‌شود.
    """
    attempt = 0
    current_worker = assigned_worker_url # تلاش اول: کارگر اختصاصی
    
    while True:
        attempt += 1
        try:
            # print(f"Processing chunk on {current_worker} (Attempt {attempt})...")
            client = Client(current_worker)
            
            result = client.predict(
                audio_files=[handle_file(chunk_path)],
                file_m=handle_file(model_path),
                file_index=handle_file(index_path) if index_path else None,
                **params,
                type_output="wav",
                steps=1,
                api_name="/run"
            )
            
            output_file = result[0] if isinstance(result, list) and result else (result if isinstance(result, str) else None)
            
            if output_file and os.path.exists(output_file):
                if os.path.getsize(output_file) > 1000:
                    return output_file
            
            print(f"Worker {current_worker} returned invalid file. Switching worker...")
            
        except Exception as e:
            print(f"Error on {current_worker}: {e}. Switching worker...")
        
        # اگر تلاش اول شکست خورد، کارگر بعدی را تصادفی انتخاب کن
        # (برای جلوگیری از قفل شدن روی یک کارگر خراب)
        time.sleep(2)
        current_worker = random.choice(all_worker_urls)

# --- تسک اصلی ---
async def process_rvc_task(job_id, audio_path_str, model_url, index_url_input, pitch, algo, index_inf, res_filter, env_ratio, protect, denoise, reverb):
    JOBS[job_id]["status"] = "processing"
    JOBS[job_id]["log"] = "شروع عملیات..."
    
    audio_path = Path(audio_path_str)
    files_to_clean = [audio_path]
    dirs_to_clean = []
    chunk_paths = []

    try:
        if not audio_path.exists(): raise FileNotFoundError("فایل ورودی یافت نشد.")

        # 1. دانلود مدل
        JOBS[job_id]["log"] = "در حال دانلود مدل..."
        downloaded_path = download_file(model_url, DOWNLOADS_DIR)
        files_to_clean.append(downloaded_path)
        
        final_model_path = None
        final_index_path = None

        if downloaded_path.suffix == ".zip":
            extract_dir = DOWNLOADS_DIR / job_id
            dirs_to_clean.append(extract_dir)
            extract_dir.mkdir(exist_ok=True)
            try:
                with zipfile.ZipFile(downloaded_path, 'r') as z: z.extractall(extract_dir)
                for root, _, files in os.walk(extract_dir):
                    for file in files:
                        fp = Path(root) / file
                        if file.endswith(".pth") and not final_model_path: final_model_path = fp
                        if file.endswith(".index") and not final_index_path: final_index_path = fp
            except: pass
        else:
            final_model_path = downloaded_path

        if index_url_input and len(index_url_input) > 5:
            try:
                idx = download_file(index_url_input, DOWNLOADS_DIR)
                final_index_path = idx
                files_to_clean.append(idx)
            except: pass

        if not final_model_path: raise Exception("مدل یافت نشد.")

        # 4. برش فایل
        JOBS[job_id]["log"] = "برش و توزیع..."
        chunk_paths = await asyncio.to_thread(smart_split_audio, audio_path, job_id)
        
        for cp in chunk_paths:
            if cp != str(audio_path): files_to_clean.append(Path(cp))

        # 5. ارسال به کارگرها (با توزیع اتمیک و چرخشی)
        count = len(chunk_paths)
        JOBS[job_id]["log"] = f"پردازش {count} قطعه (صبور باشید ☘️)..."
        
        rvc_params = {
            "pitch_alg": algo,
            "pitch_lvl": float(pitch),
            "index_inf": float(index_inf),
            "r_m_f": int(res_filter),
            "e_r": float(env_ratio),
            "c_b_p": float(protect),
            "active_noise_reduce": denoise,
            "audio_effects": reverb
        }

        tasks = []
        for chunk in chunk_paths:
            # دریافت نوبت کارگر بعدی از کلاس اتمیک
            assigned_worker = await worker_manager.get_next_worker()
            
            task = asyncio.to_thread(
                process_single_chunk_sync,
                chunk,
                str(final_model_path),
                str(final_index_path) if final_index_path else None,
                rvc_params,
                assigned_worker, # کارگر اولویت اول
                WORKER_URLS      # لیست کل برای تلاش مجدد
            )
            tasks.append(task)
        
        # اجرای موازی
        processed_chunks = await asyncio.gather(*tasks)
        
        # 6. سرهم‌بندی
        JOBS[job_id]["log"] = "ترکیب نهایی..."
        final_audio_segments = []
        sr = None
        
        for output_file in processed_chunks:
            data, sample_rate = sf.read(output_file)
            if sr is None: sr = sample_rate
            final_audio_segments.append(data)
            files_to_clean.append(Path(output_file))

        full_audio = np.concatenate(final_audio_segments)
        final_filename = f"final_{job_id}.wav"
        final_path = RESULTS_DIR / final_filename
        
        sf.write(str(final_path), full_audio, sr if sr else 44100)
        
        JOBS[job_id]["status"] = "completed"
        JOBS[job_id]["filename"] = final_filename
        JOBS[job_id]["log"] = "تکمیل شد."

    except Exception as e:
        print(f"JOB ERROR: {e}")
        JOBS[job_id]["status"] = "failed"
        JOBS[job_id]["log"] = str(e)
    
    finally:
        for p in files_to_clean:
            if p and p.exists():
                try: os.remove(p)
                except: pass
        for d in dirs_to_clean:
            if d and d.exists():
                try: shutil.rmtree(d)
                except: pass

@app.get("/")
def home(request: Request): return templates.TemplateResponse("index.html", {"request": request})

@app.post("/upload")
async def create_job(background_tasks: BackgroundTasks, audio_file: UploadFile = File(...), model_url: str = Form(...), index_url: str = Form(None), pitch: int = Form(0), algo: str = Form("rmvpe+"), index_inf: float = Form(0.75), res_filter: int = Form(3), env_ratio: float = Form(0.25), protect: float = Form(0.33), denoise: bool = Form(False), reverb: bool = Form(False)):
    job_id = str(uuid.uuid4())
    filename = audio_file.filename
    ext = filename.split('.')[-1] if '.' in filename else "wav"
    audio_path = TEMP_DIR / f"{job_id}_input.{ext}"
    with open(audio_path, "wb") as f: shutil.copyfileobj(audio_file.file, f)
    if not model_url or len(model_url.strip()) < 5: return JSONResponse({"error": "لینک مدل الزامی است"}, status_code=400)
    JOBS[job_id] = {"status": "queued", "log": "در صف...", "filename": None}
    
    background_tasks.add_task(
        process_rvc_task, 
        job_id, str(audio_path), model_url.strip(), index_url.strip() if index_url else None, 
        pitch, algo, index_inf, res_filter, env_ratio, protect, denoise, reverb
    )
    return {"job_id": job_id, "status": "queued"}

@app.get("/status/{job_id}")
def check_status(job_id: str): return JOBS.get(job_id, {"status": "not_found"})

@app.get("/download/{filename}")
def download_result(filename: str):
    path = RESULTS_DIR / filename
    if path.exists(): return FileResponse(path, filename=filename, media_type="audio/wav")
    return {"error": "File not found"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=7860)