Photo / app.py
mahdis9090's picture
Update app.py
de2de95 verified
Raw
History Blame Contribute Delete
11.4 kB
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
@app.get("/")
def home(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.post("/upload")
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"
}
@app.post("/check_status")
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}
@app.get("/download/{filename}")
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)