|
|
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: |
|
|
|
|
|
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("فایل ورودی یافت نشد.") |
|
|
|
|
|
|
|
|
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("مدل یافت نشد.") |
|
|
|
|
|
|
|
|
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)) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |