Spaces:
Sleeping
Sleeping
| import os | |
| import uuid | |
| import shutil | |
| import asyncio | |
| import aiohttp | |
| import librosa | |
| import numpy as np | |
| import soundfile as sf | |
| import json | |
| from fastapi import FastAPI, UploadFile, File, Form, Body | |
| from fastapi.responses import FileResponse, JSONResponse | |
| from fastapi.templating import Jinja2Templates | |
| from fastapi.requests import Request | |
| from pydantic import BaseModel | |
| from typing import List, Optional | |
| app = FastAPI() | |
| # --- لیست کارگرها --- | |
| WORKER_URLS_LIST = [ | |
| "https://ezmary-taqviat-sadaworker1.hf.space", | |
| "https://ezmary-taqviat-sadaworker2.hf.space", | |
| "https://ezmary-taqviat-sadaworker3.hf.space", | |
| "https://ezmary-taqviat-sadaworker4.hf.space", | |
| "https://ezmary-taqviat-sadaworker5.hf.space", | |
| "https://eltafjan-taqviat-sadaworker6.hf.space", | |
| "https://eltafjan-taqviat-sadaworker7.hf.space", | |
| "https://eltafjan-taqviat-sadaworker8.hf.space", | |
| "https://eltafjan-taqviat-sadaworker9.hf.space", | |
| "https://eltafjan-taqviat-sadaworker10.hf.space", | |
| "https://eltafjan-taqviat-sadaworker11.hf.space", | |
| "https://eltafjan-taqviat-sadaworker12.hf.space", | |
| "https://eltafjan-taqviat-sadaworker13.hf.space", | |
| "https://eltafjan-taqviat-sadaworker14.hf.space", | |
| "https://eltafjan-taqviat-sadaworker15.hf.space", | |
| "https://eltafjan-taqviat-sadaworker16.hf.space", | |
| "https://eltafjan-taqviat-sadaworker17.hf.space", | |
| "https://eltafjan-taqviat-sadaworker18.hf.space", | |
| "https://eltafjan-taqviat-sadaworker19.hf.space", | |
| "https://eltafjan-taqviat-sadaworker20.hf.space" | |
| ] | |
| os.makedirs("temp", exist_ok=True) | |
| os.makedirs("results", exist_ok=True) | |
| templates = Jinja2Templates(directory="templates") | |
| # --- مدیریت کارگرها (چرخشی) --- | |
| class AtomicWorkerManager: | |
| def __init__(self, urls): | |
| self.urls = urls | |
| self.total_workers = len(urls) | |
| self.current_index = 0 | |
| self.lock = asyncio.Lock() | |
| async def get_next_worker(self): | |
| async with self.lock: | |
| url = self.urls[self.current_index] | |
| self.current_index = (self.current_index + 1) % self.total_workers | |
| return url | |
| worker_manager = AtomicWorkerManager(WORKER_URLS_LIST) | |
| # --- مدلهای داده --- | |
| class ChunkInfo(BaseModel): | |
| index: int | |
| worker_url: str | |
| task_id: str | |
| class ProjectState(BaseModel): | |
| job_id: str | |
| total_chunks: int | |
| chunks: List[ChunkInfo] | |
| # --- توابع کمکی --- | |
| def find_split_points(audio_path, sr=24000): | |
| try: | |
| y, _ = librosa.load(audio_path, sr=sr) | |
| except: | |
| data, samplerate = sf.read(audio_path) | |
| if len(data.shape) > 1: data = np.mean(data, axis=1) | |
| if samplerate != sr: | |
| # Simple resampling if needed, ideally use librosa.resample | |
| # But here relying on librosa.load mostly | |
| pass | |
| y = data | |
| total_samples = len(y) | |
| split_points = [0] | |
| current_pos = 0 | |
| # منطق برش: بین 30 تا 60 ثانیه | |
| min_duration = 30 * sr | |
| max_duration = 60 * sr | |
| while current_pos < total_samples: | |
| # هدف نهایی (60 ثانیه بعد) | |
| target = current_pos + max_duration | |
| # اگر به انتهای فایل نزدیکیم | |
| if target >= total_samples: | |
| split_points.append(total_samples) | |
| break | |
| # بازه جستجو برای سکوت: از ثانیه 30 تا 60 | |
| search_start = current_pos + min_duration | |
| search_end = target # تا خود 60 ثانیه | |
| if search_start >= total_samples: | |
| split_points.append(total_samples) | |
| break | |
| region = y[search_start:search_end] | |
| # پیدا کردن کمترین انرژی (سکوت) | |
| if len(region) > 0: | |
| rms = librosa.feature.rms(y=region, frame_length=1024, hop_length=512)[0] | |
| min_idx = np.argmin(rms) | |
| # تبدیل ایندکس فریم به ایندکس سمپل | |
| cut_point = search_start + (min_idx * 512) | |
| else: | |
| cut_point = target | |
| split_points.append(cut_point) | |
| current_pos = cut_point | |
| return split_points, y | |
| async def submit_to_worker(session, worker_url, chunk_path, params): | |
| try: | |
| with open(chunk_path, 'rb') as f_c: | |
| data = aiohttp.FormData() | |
| data.add_field('audio_file', f_c, filename='input.wav', content_type='audio/wav') | |
| # ارسال تنظیمات | |
| data.add_field('solver', params.get('solver', 'Midpoint')) | |
| data.add_field('nfe', str(params.get('nfe', 64))) | |
| data.add_field('tau', str(params.get('tau', 0.5))) | |
| data.add_field('denoising', str(params.get('denoising', 'false'))) | |
| async with session.post(f"{worker_url}/submit", data=data, timeout=120) as resp: | |
| if resp.status == 200: | |
| js = await resp.json() | |
| return js.get("task_id") | |
| except Exception as e: | |
| print(f"Submit error to {worker_url}: {e}") | |
| return None | |
| return None | |
| async def check_worker_status(session, worker_url, task_id): | |
| try: | |
| async with session.get(f"{worker_url}/result/{task_id}") as resp: | |
| if resp.status == 200: | |
| return "completed", await resp.read() | |
| elif resp.status == 202: | |
| return "processing", None | |
| elif resp.status == 404: | |
| return "processing", None | |
| else: | |
| return "failed", None | |
| except: | |
| return "processing", None | |
| def home(request: Request): | |
| return templates.TemplateResponse("index.html", {"request": request}) | |
| async def start_process( | |
| source_audio: UploadFile = File(...), | |
| solver: str = Form("Midpoint"), | |
| nfe: int = Form(64), | |
| tau: float = Form(0.5), | |
| denoising: bool = Form(False) | |
| ): | |
| job_id = str(uuid.uuid4()) | |
| os.makedirs(f"temp/{job_id}", exist_ok=True) | |
| src_path = f"temp/{job_id}/src.wav" | |
| with open(src_path, "wb") as b: shutil.copyfileobj(source_audio.file, b) | |
| # تنظیمات برای ارسال به کارگر | |
| worker_params = { | |
| "solver": solver, | |
| "nfe": nfe, | |
| "tau": tau, | |
| "denoising": "true" if denoising else "false" | |
| } | |
| sr = 44100 # Resemble معمولا با 44100 کار میکند | |
| # برش فایل | |
| split_points, y = find_split_points(src_path, sr) | |
| total_chunks = len(split_points) - 1 | |
| chunks_metadata = [] | |
| async with aiohttp.ClientSession() as session: | |
| tasks = [] | |
| for i in range(total_chunks): | |
| start = split_points[i] | |
| end = split_points[i+1] | |
| chunk_audio = y[start:end] | |
| # رد کردن تکههای خیلی کوتاه (زیر 0.5 ثانیه) | |
| if len(chunk_audio) < 0.5 * sr: | |
| chunks_metadata.append({"index": i, "worker_url": "skip", "task_id": "skip"}) | |
| continue | |
| chunk_path = f"temp/{job_id}/chunk_{i}.wav" | |
| sf.write(chunk_path, chunk_audio, sr) | |
| worker_url = await worker_manager.get_next_worker() | |
| tasks.append(submit_to_worker(session, worker_url, chunk_path, worker_params)) | |
| chunks_metadata.append({ | |
| "index": i, | |
| "worker_url": worker_url, | |
| "task_id": "pending" | |
| }) | |
| results = await asyncio.gather(*tasks) | |
| active_task_idx = 0 | |
| for i in range(len(chunks_metadata)): | |
| if chunks_metadata[i]["task_id"] == "skip": continue | |
| task_id = results[active_task_idx] | |
| active_task_idx += 1 | |
| if task_id: | |
| chunks_metadata[i]["task_id"] = task_id | |
| else: | |
| chunks_metadata[i]["task_id"] = "failed" | |
| return { | |
| "job_id": job_id, | |
| "total_chunks": total_chunks, | |
| "chunks": chunks_metadata, | |
| "status": "started" | |
| } | |
| async def check_status(project: ProjectState): | |
| final_filename = f"enhanced_{project.job_id}.wav" | |
| if os.path.exists(f"results/{final_filename}"): | |
| return {"status": "completed", "progress": 100, "filename": final_filename} | |
| completed_count = 0 | |
| audio_parts = {} | |
| failed_any = False | |
| async with aiohttp.ClientSession() as session: | |
| tasks = [] | |
| task_indices = [] | |
| for chunk in project.chunks: | |
| if chunk.task_id == "skip": | |
| completed_count += 1 | |
| audio_parts[chunk.index] = None | |
| continue | |
| if chunk.task_id == "failed": | |
| failed_any = True | |
| continue | |
| tasks.append(check_worker_status(session, chunk.worker_url, chunk.task_id)) | |
| task_indices.append(chunk.index) | |
| results = await asyncio.gather(*tasks) | |
| for i, (status, data) in enumerate(results): | |
| idx = task_indices[i] | |
| if status == "completed" and data: | |
| completed_count += 1 | |
| audio_parts[idx] = data | |
| elif status == "failed": | |
| failed_any = True | |
| progress = int((completed_count / project.total_chunks) * 100) | |
| if completed_count == project.total_chunks or (completed_count > 0 and progress > 98 and failed_any): | |
| try: | |
| full_audio = [] | |
| # استفاده از سمپل ریت خروجی Resemble (معمولا 44100) | |
| target_sr = 44100 | |
| for i in range(project.total_chunks): | |
| if i in audio_parts: | |
| if isinstance(audio_parts[i], bytes): | |
| tmp_path = f"temp/part_{project.job_id}_{i}.wav" | |
| with open(tmp_path, "wb") as f: f.write(audio_parts[i]) | |
| y, sr = librosa.load(tmp_path, sr=None) # Load native SR | |
| full_audio.append(y) | |
| target_sr = sr # Update SR based on worker output | |
| os.remove(tmp_path) | |
| else: | |
| pass # Skip part (silence?) | |
| else: | |
| # Missing part due to error | |
| full_audio.append(np.zeros(int(target_sr * 1.0))) # 1 sec silence | |
| final_wav = np.concatenate(full_audio) | |
| sf.write(f"results/{final_filename}", final_wav, target_sr) | |
| shutil.rmtree(f"temp/{project.job_id}", ignore_errors=True) | |
| return {"status": "completed", "progress": 100, "filename": final_filename} | |
| except Exception as e: | |
| print(f"Stitch error: {e}") | |
| return {"status": "error", "progress": progress, "detail": str(e)} | |
| else: | |
| return {"status": "processing", "progress": progress} | |
| def download_file(filename: str): | |
| path = f"results/{filename}" | |
| if os.path.exists(path): | |
| 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) | |